From ab9267bf8e0f1f96f42f567df21cf75dc07f1101 Mon Sep 17 00:00:00 2001 From: amabito <192487536+amabito@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:34:48 +0900 Subject: [PATCH] refactor: unify AbortError detection with isAbortError utility AbortError detection was scattered across 3+ patterns (string comparison, .name check, .code check). Consolidates into a single isAbortError() utility that handles all known patterns: "cancel" string, DOMException, Error.name variants, ABORT_ERR code, and plain objects. Updates call sites in core/llm/index.ts and core/llm/utils/retry.ts. --- core/llm/index.ts | 5 +- core/llm/utils/retry.ts | 4 +- core/util/isAbortError.ts | 30 ++++++++++++ core/util/isAbortError.vitest.ts | 78 ++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 core/util/isAbortError.ts create mode 100644 core/util/isAbortError.vitest.ts diff --git a/core/llm/index.ts b/core/llm/index.ts index dd6dd9c00aa..08d23442e10 100644 --- a/core/llm/index.ts +++ b/core/llm/index.ts @@ -31,6 +31,7 @@ import { ToolOverride, Usage, } from "../index.js"; +import { isAbortError } from "../util/isAbortError.js"; import { isLemonadeInstalled } from "../util/lemonadeHelper.js"; import { Logger } from "../util/Logger.js"; import mergeJson from "../util/merge.js"; @@ -394,7 +395,7 @@ export abstract class BaseLLM implements ILLM { }); return "success"; } else { - if (error === "cancel" || error?.name?.includes("AbortError")) { + if (isAbortError(error)) { interaction?.logItem({ kind: "cancel", promptTokens, @@ -502,7 +503,7 @@ export abstract class BaseLLM implements ILLM { `HTTP ${e.response.status} ${e.response.statusText} from ${e.response.url}\n\n${e.response.body}`, ); } else { - if (e.name !== "AbortError") { + if (!isAbortError(e)) { // Don't pollute console with abort errors. Check on name instead of instanceof, to avoid importing node-fetch here console.debug( `${e.message}\n\nCode: ${e.code}\nError number: ${e.errno}\nSyscall: ${e.erroredSysCall}\nType: ${e.type}\n\n${e.stack}`, diff --git a/core/llm/utils/retry.ts b/core/llm/utils/retry.ts index dce06ef5c24..45ee3dbef88 100644 --- a/core/llm/utils/retry.ts +++ b/core/llm/utils/retry.ts @@ -1,3 +1,5 @@ +import { isAbortError } from "../../util/isAbortError.js"; + /** * Configuration options for the retry decorator */ @@ -98,7 +100,7 @@ function defaultShouldRetry(error: any, attempt: number): boolean { } // Abort signal errors should not be retried - if (error.name === "AbortError" || error.code === "ABORT_ERR") { + if (isAbortError(error)) { return false; } diff --git a/core/util/isAbortError.ts b/core/util/isAbortError.ts new file mode 100644 index 00000000000..ba57fe57735 --- /dev/null +++ b/core/util/isAbortError.ts @@ -0,0 +1,30 @@ +/** + * Unified abort error detection. + * Handles all known abort error patterns across different environments. + */ +export function isAbortError(error: unknown): boolean { + // String-based "cancel" (used in Continue codebase) + if (error === "cancel") return true; + + // Standard Error objects + if (error instanceof Error) { + if (error.name.includes("AbortError")) return true; + if ("code" in error && (error as any).code === "ABORT_ERR") return true; + } + + // DOMException (browser/Node.js 18+) + if ( + typeof DOMException !== "undefined" && + error instanceof DOMException && + error.name === "AbortError" + ) + return true; + + // Handle plain objects with name property + if (typeof error === "object" && error !== null && "name" in error) { + const name = (error as any).name; + if (typeof name === "string" && name.includes("AbortError")) return true; + } + + return false; +} diff --git a/core/util/isAbortError.vitest.ts b/core/util/isAbortError.vitest.ts new file mode 100644 index 00000000000..d53bea56665 --- /dev/null +++ b/core/util/isAbortError.vitest.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; +import { isAbortError } from "./isAbortError"; + +describe("isAbortError", () => { + it('should return true for "cancel" string', () => { + expect(isAbortError("cancel")).toBe(true); + }); + + it("should return true for DOMException with AbortError name", () => { + if (typeof DOMException !== "undefined") { + const error = new DOMException("Operation aborted", "AbortError"); + expect(isAbortError(error)).toBe(true); + } else { + // Skip in environments without DOMException + expect(true).toBe(true); + } + }); + + it("should return true for Error with AbortError name", () => { + const error = Object.assign(new Error("aborted"), { name: "AbortError" }); + expect(isAbortError(error)).toBe(true); + }); + + it("should return true for Error with name containing AbortError (middle match)", () => { + const error = Object.assign(new Error("aborted"), { + name: "NetworkAbortError", + }); + expect(isAbortError(error)).toBe(true); + }); + + it("should return true for Error with name starting with AbortError", () => { + const error = Object.assign(new Error("aborted"), { + name: "AbortErrorSomething", + }); + expect(isAbortError(error)).toBe(true); + }); + + it("should return true for Error with ABORT_ERR code", () => { + const error = Object.assign(new Error("aborted"), { code: "ABORT_ERR" }); + expect(isAbortError(error)).toBe(true); + }); + + it("should return true for plain object with AbortError name", () => { + const error = { name: "AbortError" }; + expect(isAbortError(error)).toBe(true); + }); + + it("should return true for plain object with name containing AbortError (middle match)", () => { + const error = { name: "NetworkAbortError" }; + expect(isAbortError(error)).toBe(true); + }); + + it("should return true for plain object with name starting with AbortError", () => { + const error = { name: "AbortErrorNetwork" }; + expect(isAbortError(error)).toBe(true); + }); + + it("should return false for regular Error", () => { + const error = new Error("network error"); + expect(isAbortError(error)).toBe(false); + }); + + it("should return false for null", () => { + expect(isAbortError(null)).toBe(false); + }); + + it("should return false for undefined", () => { + expect(isAbortError(undefined)).toBe(false); + }); + + it("should return false for number", () => { + expect(isAbortError(42)).toBe(false); + }); + + it("should return false for plain object without name", () => { + expect(isAbortError({ message: "error" })).toBe(false); + }); +});