From 5bb969f7de6cb07747e746e90d6444bb935dcb8e Mon Sep 17 00:00:00 2001 From: Wynne668 Date: Sun, 19 Jul 2026 10:32:04 +0800 Subject: [PATCH] fix(tlon): preserve shutdown when body cancellation fails (#111106) * fix(tlon): observe shutdown body cancellation failures * test(tlon): streamline shutdown cleanup proof * test(tlon): narrow shutdown fixtures --------- Co-authored-by: ZengWen-DT <290981215+ZengWen-DT@users.noreply.github.com> Co-authored-by: Peter Steinberger --- extensions/tlon/src/urbit/sse-client.test.ts | 58 ++++++++++++++++++++ extensions/tlon/src/urbit/sse-client.ts | 4 +- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/extensions/tlon/src/urbit/sse-client.test.ts b/extensions/tlon/src/urbit/sse-client.test.ts index d3efa0838e20..47df7d170e96 100644 --- a/extensions/tlon/src/urbit/sse-client.test.ts +++ b/extensions/tlon/src/urbit/sse-client.test.ts @@ -159,6 +159,64 @@ describe("UrbitSSEClient", () => { }); }); + describe("close", () => { + it("releases shutdown responses without leaking cancellation failures", async () => { + const unhandledRejections: unknown[] = []; + const onUnhandledRejection = (reason: unknown) => { + unhandledRejections.push(reason); + }; + const shutdownResponses = [ + { method: "PUT", label: "unsubscribe" }, + { method: "DELETE", label: "delete" }, + ].map(({ method, label }) => { + const cancel = vi.fn(() => { + throw new Error(`${label} cancel failed`); + }); + const release = vi.fn().mockResolvedValue(undefined); + return { + method, + cancel, + release, + result: { + response: new Response(new ReadableStream({ cancel })), + finalUrl: "https://example.com", + release, + }, + }; + }); + const [unsubscribeResponse, deleteResponse] = shutdownResponses; + if (!unsubscribeResponse || !deleteResponse) { + throw new Error("Expected both shutdown response fixtures"); + } + const mockUrbitFetch = vi.mocked(urbitFetch); + mockUrbitFetch + .mockResolvedValueOnce(unsubscribeResponse.result) + .mockResolvedValueOnce(deleteResponse.result); + const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", { + autoReconnect: false, + }); + process.on("unhandledRejection", onUnhandledRejection); + + try { + await client.close(); + + expect(mockUrbitFetch).toHaveBeenCalledTimes(2); + for (const [index, { method, cancel, release }] of shutdownResponses.entries()) { + expect(mockUrbitFetch.mock.calls[index]?.[0].init?.method).toBe(method); + expect(cancel).toHaveBeenCalledOnce(); + expect(release).toHaveBeenCalledOnce(); + } + await new Promise((resolve) => { + setImmediate(resolve); + }); + expect(unhandledRejections).toStrictEqual([]); + } finally { + process.off("unhandledRejection", onUnhandledRejection); + expect(process.listeners("unhandledRejection")).not.toContain(onUnhandledRejection); + } + }); + }); + describe("reconnection", () => { it("has autoReconnect enabled by default", () => { const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123"); diff --git a/extensions/tlon/src/urbit/sse-client.ts b/extensions/tlon/src/urbit/sse-client.ts index 744f448cc8b4..2689d41786e7 100644 --- a/extensions/tlon/src/urbit/sse-client.ts +++ b/extensions/tlon/src/urbit/sse-client.ts @@ -547,7 +547,7 @@ export class UrbitSSEClient { auditContext: "tlon-urbit-unsubscribe", }); try { - void response.body?.cancel(); + void response.body?.cancel().catch(() => undefined); } finally { await release(); } @@ -570,7 +570,7 @@ export class UrbitSSEClient { auditContext: "tlon-urbit-channel-close", }); try { - void response.body?.cancel(); + void response.body?.cancel().catch(() => undefined); } finally { await release(); }