diff --git a/packages/sdk/js/script/build.ts b/packages/sdk/js/script/build.ts index 79e0879c9e14..95428209d4b5 100755 --- a/packages/sdk/js/script/build.ts +++ b/packages/sdk/js/script/build.ts @@ -112,6 +112,20 @@ if (sseTypesPatched === sseTypesSource) { } await Bun.write(sseTypesPath, sseTypesPatched) +// Abort handlers cannot await ReadableStreamDefaultReader.cancel(), but the +// returned promise can reject after the body stream has already errored. +const sseClientPath = "./src/v2/gen/core/serverSentEvents.gen.ts" +const sseClientFile = Bun.file(sseClientPath) +const sseClientSource = await sseClientFile.text() +const sseClientPatched = sseClientSource.replace( + " reader.cancel()", + " void reader.cancel().catch(() => {})", +) +if (sseClientPatched === sseClientSource) { + throw new Error(`SSE cancel patch did not apply; @hey-api/openapi-ts output may have changed (${sseClientPath})`) +} +await Bun.write(sseClientPath, sseClientPatched) + await $`bun prettier --write src/gen` await $`bun prettier --write src/v2` await $`rm -rf dist` diff --git a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts index ffc4f16dc1f3..a631612b0793 100644 --- a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts @@ -111,7 +111,7 @@ export const createSseClient = ({ const abortHandler = () => { try { - void reader.cancel() + void reader.cancel().catch(() => {}) } catch { // noop } diff --git a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts index 056a81259322..05e6dd371dba 100644 --- a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts @@ -138,7 +138,7 @@ export const createSseClient = ({ const abortHandler = () => { try { - reader.cancel() + void reader.cancel().catch(() => {}) } catch { // noop } diff --git a/packages/sdk/js/test/server-sent-events.test.ts b/packages/sdk/js/test/server-sent-events.test.ts new file mode 100644 index 000000000000..55e109b05b84 --- /dev/null +++ b/packages/sdk/js/test/server-sent-events.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "bun:test" +import { createSseClient } from "../src/v2/gen/core/serverSentEvents.gen" + +test("observes reader cancellation errors when an SSE request is aborted", async () => { + const abort = new AbortController() + const result = createSseClient({ + fetch: async () => + new Response( + new ReadableStream({ + start: (controller) => + abort.signal.addEventListener("abort", () => + controller.error(new DOMException("The operation was aborted", "AbortError")), + ), + }), + { headers: { "content-type": "text/event-stream" } }, + ), + method: "GET", + signal: abort.signal, + sseMaxRetryAttempts: 1, + url: "http://localhost/events", + }) + + const next = result.stream.next() + // Let the generator park in reader.read() before erroring the body stream. + await Bun.sleep(10) + abort.abort() + expect(await next).toEqual({ done: true, value: undefined }) + // Give any unobserved cancellation rejection time to reach Bun's test runner. + await Bun.sleep(10) +})