Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-abort-signal-reason.md
Original file line number Diff line number Diff line change
@@ -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
23 changes: 19 additions & 4 deletions packages/fetch-http-handler/src/fetch-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ export class FetchHttpHandler implements HttpHandler<FetchHttpHandlerOptions> {

// 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);
}

Expand Down Expand Up @@ -185,8 +184,7 @@ export class FetchHttpHandler implements HttpHandler<FetchHttpHandlerOptions> {
raceOfPromises.push(
new Promise<never>((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") {
Expand Down Expand Up @@ -216,3 +214,20 @@ export class FetchHttpHandler implements HttpHandler<FetchHttpHandlerOptions> {
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;
}
46 changes: 46 additions & 0 deletions packages/node-http-handler/src/node-http-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
23 changes: 19 additions & 4 deletions packages/node-http-handler/src/node-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should appear only once in this package

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;
}
23 changes: 19 additions & 4 deletions packages/node-http-handler/src/node-http2-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ export class NodeHttp2Handler implements HttpHandler<NodeHttp2HandlerOptions> {
// 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;
}
Expand Down Expand Up @@ -194,8 +193,7 @@ export class NodeHttp2Handler implements HttpHandler<NodeHttp2HandlerOptions> {
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") {
Expand Down Expand Up @@ -261,3 +259,20 @@ export class NodeHttp2Handler implements HttpHandler<NodeHttp2HandlerOptions> {
}
}
}

/**
* 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;
}