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
5 changes: 3 additions & 2 deletions core/llm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}`,
Expand Down
4 changes: 3 additions & 1 deletion core/llm/utils/retry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isAbortError } from "../../util/isAbortError.js";

/**
* Configuration options for the retry decorator
*/
Expand Down Expand Up @@ -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;
}

Expand Down
30 changes: 30 additions & 0 deletions core/util/isAbortError.ts
Original file line number Diff line number Diff line change
@@ -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;
}
78 changes: 78 additions & 0 deletions core/util/isAbortError.vitest.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading