From 94d51c8c07869c11ad79a2043b9fe8621a605ca8 Mon Sep 17 00:00:00 2001 From: Will King Date: Mon, 11 May 2026 11:22:33 -0500 Subject: [PATCH] Fix RpcServer toHttpEffect lifetime --- packages/effect/src/unstable/rpc/RpcServer.ts | 5 ++- packages/effect/test/rpc/RpcServer.test.ts | 43 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 packages/effect/test/rpc/RpcServer.test.ts diff --git a/packages/effect/src/unstable/rpc/RpcServer.ts b/packages/effect/src/unstable/rpc/RpcServer.ts index 929d0418ad..5f0bb90455 100644 --- a/packages/effect/src/unstable/rpc/RpcServer.ts +++ b/packages/effect/src/unstable/rpc/RpcServer.ts @@ -980,6 +980,9 @@ export const makeProtocolWithHttpEffect: Effect.Effect< } yield* Scope.addFinalizerExit(scope, () => { + if (!includesFraming) { + return Effect.void + } clients.delete(id) clientIds.delete(id) Queue.offerUnsafe(disconnects, id) @@ -1135,7 +1138,7 @@ export const toHttpEffect: ( const { httpEffect, protocol } = yield* makeProtocolWithHttpEffect yield* make(group, options).pipe( Effect.provideService(Protocol, protocol), - Effect.forkScoped + Effect.forkDetach({ startImmediately: true }) ) // @effect-diagnostics-next-line returnEffectInGen:off return httpEffect diff --git a/packages/effect/test/rpc/RpcServer.test.ts b/packages/effect/test/rpc/RpcServer.test.ts new file mode 100644 index 0000000000..82569e19d3 --- /dev/null +++ b/packages/effect/test/rpc/RpcServer.test.ts @@ -0,0 +1,43 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect, Schema } from "effect" +import { HttpEffect } from "effect/unstable/http" +import { Rpc, RpcGroup, RpcSerialization, RpcServer } from "effect/unstable/rpc" + +const TestGroup = RpcGroup.make( + Rpc.make("Ping", { success: Schema.String }) +) + +describe("RpcServer", () => { + it.effect("toHttpEffect keeps non-framed json-rpc server alive outside construction scope", () => + Effect.gen(function*() { + const httpEffect = yield* RpcServer.toHttpEffect(TestGroup).pipe( + Effect.provide(TestGroup.toLayer({ + Ping: () => Effect.succeed("pong") + })), + Effect.provideService(RpcSerialization.RpcSerialization, RpcSerialization.jsonRpc()), + Effect.scoped + ) + + const handler = HttpEffect.toWebHandler(httpEffect) + const response = yield* Effect.promise(() => + handler( + new Request("http://localhost/rpc", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "Ping" + }) + }) + ) + ) + + assert.strictEqual(response.status, 200) + assert.deepStrictEqual(yield* Effect.promise(() => response.json()), { + jsonrpc: "2.0", + id: 1, + result: "pong" + }) + }).pipe(Effect.timeout("2 seconds"))) +})