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 <steipete@gmail.com>
This commit is contained in:
Wynne668
2026-07-19 10:32:04 +08:00
committed by GitHub
parent 18b79d99ab
commit 5bb969f7de
2 changed files with 60 additions and 2 deletions

View File

@@ -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<Uint8Array>({ 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<void>((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");

View File

@@ -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();
}