From 38a4b4b6fa7981d9d2a9a96cb0fefc2cb87f5f88 Mon Sep 17 00:00:00 2001 From: Trevor Burnham Date: Sat, 21 Feb 2026 16:22:18 -0500 Subject: [PATCH] fix: reject aborted requests with AbortSignal.reason When a request is aborted, the http handlers now use the AbortSignal's reason property (if available) instead of always creating a generic Error('Request aborted'). This lets callers using AbortSignal.any() identify which signal caused the abort. If reason is an Error instance, it is used directly. If it is a non-Error value (e.g. a string), it is wrapped in an Error with name 'AbortError'. If no reason is set, the existing behavior is preserved with the default 'Request aborted' message. Applied to all three handlers: - NodeHttpHandler (http/https) - NodeHttp2Handler (http2) - FetchHttpHandler (fetch) Fixes smithy-lang/smithy-typescript#1360 --- .changeset/fix-abort-signal-reason.md | 6 +++ .../src/fetch-http-handler.ts | 23 ++++++++-- .../src/node-http-handler.spec.ts | 46 +++++++++++++++++++ .../src/node-http-handler.ts | 23 ++++++++-- .../src/node-http2-handler.ts | 23 ++++++++-- 5 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 .changeset/fix-abort-signal-reason.md diff --git a/.changeset/fix-abort-signal-reason.md b/.changeset/fix-abort-signal-reason.md new file mode 100644 index 00000000000..a98b712cdd6 --- /dev/null +++ b/.changeset/fix-abort-signal-reason.md @@ -0,0 +1,6 @@ +--- +"@smithy/node-http-handler": patch +"@smithy/fetch-http-handler": patch +--- + +fix: reject aborted requests with AbortSignal.reason instead of a generic Error diff --git a/packages/fetch-http-handler/src/fetch-http-handler.ts b/packages/fetch-http-handler/src/fetch-http-handler.ts index 536799aa4fc..68c6ab8bbe0 100644 --- a/packages/fetch-http-handler/src/fetch-http-handler.ts +++ b/packages/fetch-http-handler/src/fetch-http-handler.ts @@ -87,8 +87,7 @@ export class FetchHttpHandler implements HttpHandler { // if the request was already aborted, prevent doing extra work if (abortSignal?.aborted) { - const abortError = new Error("Request aborted"); - abortError.name = "AbortError"; + const abortError = buildAbortError(abortSignal); return Promise.reject(abortError); } @@ -185,8 +184,7 @@ export class FetchHttpHandler implements HttpHandler { raceOfPromises.push( new Promise((resolve, reject) => { const onAbort = () => { - const abortError = new Error("Request aborted"); - abortError.name = "AbortError"; + const abortError = buildAbortError(abortSignal); reject(abortError); }; if (typeof (abortSignal as AbortSignal).addEventListener === "function") { @@ -216,3 +214,20 @@ export class FetchHttpHandler implements HttpHandler { return this.config ?? {}; } } + +/** + * Builds an abort error, using the AbortSignal's reason if available. + */ +function buildAbortError(abortSignal?: { reason?: unknown }): Error { + if (abortSignal?.reason) { + if (abortSignal.reason instanceof Error) { + return abortSignal.reason; + } + const abortError = new Error(String(abortSignal.reason)); + abortError.name = "AbortError"; + return abortError; + } + const abortError = new Error("Request aborted"); + abortError.name = "AbortError"; + return abortError; +} diff --git a/packages/node-http-handler/src/node-http-handler.spec.ts b/packages/node-http-handler/src/node-http-handler.spec.ts index 5a7943511d6..e3ba1ec61c1 100644 --- a/packages/node-http-handler/src/node-http-handler.spec.ts +++ b/packages/node-http-handler/src/node-http-handler.spec.ts @@ -410,6 +410,52 @@ describe("NodeHttpHandler", () => { }); }); + describe("abort signal handling", () => { + it("rejects with AbortSignal.reason when signal is already aborted with a custom reason", async () => { + const nodeHttpHandler = new NodeHttpHandler(); + const customReason = new Error("custom abort reason"); + customReason.name = "CustomAbortError"; + const abortController = new AbortController(); + abortController.abort(customReason); + + await expect( + nodeHttpHandler.handle({ protocol: "http:", hostname: "host", path: "/", headers: {} } as any, { + abortSignal: abortController.signal as any, + }) + ).rejects.toBe(customReason); + }); + + it("rejects with default AbortError when signal has no reason", async () => { + const nodeHttpHandler = new NodeHttpHandler(); + const signal = { + aborted: true, + onabort: null, + }; + + await expect( + nodeHttpHandler.handle({ protocol: "http:", hostname: "host", path: "/", headers: {} } as any, { + abortSignal: signal as any, + }) + ).rejects.toThrow("Request aborted"); + }); + + it("rejects with string reason wrapped in an Error", async () => { + const nodeHttpHandler = new NodeHttpHandler(); + const abortController = new AbortController(); + abortController.abort("string reason"); + + try { + await nodeHttpHandler.handle({ protocol: "http:", hostname: "host", path: "/", headers: {} } as any, { + abortSignal: abortController.signal as any, + }); + expect.unreachable("should have thrown"); + } catch (e: any) { + expect(e.message).toBe("string reason"); + expect(e.name).toBe("AbortError"); + } + }); + }); + describe("checkSocketUsage", () => { beforeEach(() => { vi.spyOn(console, "warn").mockImplementation(vi.fn() as any); diff --git a/packages/node-http-handler/src/node-http-handler.ts b/packages/node-http-handler/src/node-http-handler.ts index f5554925119..9a4875e86e1 100644 --- a/packages/node-http-handler/src/node-http-handler.ts +++ b/packages/node-http-handler/src/node-http-handler.ts @@ -196,8 +196,7 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf // if the request was already aborted, prevent doing extra work if (abortSignal?.aborted) { - const abortError = new Error("Request aborted"); - abortError.name = "AbortError"; + const abortError = buildAbortError(abortSignal); reject(abortError); return; } @@ -294,8 +293,7 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf const onAbort = () => { // ensure request is destroyed req.destroy(); - const abortError = new Error("Request aborted"); - abortError.name = "AbortError"; + const abortError = buildAbortError(abortSignal); reject(abortError); }; if (typeof (abortSignal as AbortSignal).addEventListener === "function") { @@ -354,3 +352,20 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf return this.config ?? {}; } } + +/** + * Builds an abort error, using the AbortSignal's reason if available. + */ +function buildAbortError(abortSignal?: { reason?: unknown }): Error { + if (abortSignal?.reason) { + if (abortSignal.reason instanceof Error) { + return abortSignal.reason; + } + const abortError = new Error(String(abortSignal.reason)); + abortError.name = "AbortError"; + return abortError; + } + const abortError = new Error("Request aborted"); + abortError.name = "AbortError"; + return abortError; +} diff --git a/packages/node-http-handler/src/node-http2-handler.ts b/packages/node-http-handler/src/node-http2-handler.ts index e195de4efee..e9d458605a5 100644 --- a/packages/node-http-handler/src/node-http2-handler.ts +++ b/packages/node-http-handler/src/node-http2-handler.ts @@ -120,8 +120,7 @@ export class NodeHttp2Handler implements HttpHandler { // if the request was already aborted, prevent doing extra work if (abortSignal?.aborted) { fulfilled = true; - const abortError = new Error("Request aborted"); - abortError.name = "AbortError"; + const abortError = buildAbortError(abortSignal); reject(abortError); return; } @@ -194,8 +193,7 @@ export class NodeHttp2Handler implements HttpHandler { if (abortSignal) { const onAbort = () => { req.close(); - const abortError = new Error("Request aborted"); - abortError.name = "AbortError"; + const abortError = buildAbortError(abortSignal); rejectWithDestroy(abortError); }; if (typeof (abortSignal as AbortSignal).addEventListener === "function") { @@ -261,3 +259,20 @@ export class NodeHttp2Handler implements HttpHandler { } } } + +/** + * Builds an abort error, using the AbortSignal's reason if available. + */ +function buildAbortError(abortSignal?: { reason?: unknown }): Error { + if (abortSignal?.reason) { + if (abortSignal.reason instanceof Error) { + return abortSignal.reason; + } + const abortError = new Error(String(abortSignal.reason)); + abortError.name = "AbortError"; + return abortError; + } + const abortError = new Error("Request aborted"); + abortError.name = "AbortError"; + return abortError; +}