Skip to content
Closed
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
64 changes: 64 additions & 0 deletions core/llm/utils/retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,70 @@ describe("Retry Functionality", () => {
expect(mockFn).toHaveBeenCalledTimes(2);
});

it("should retry on premature close error", async () => {
const error = new Error("Premature close");
const mockFn = jest
.fn()
.mockRejectedValueOnce(error)
.mockResolvedValue("success");

const result = await retryAsync(mockFn, {
maxAttempts: 2,
baseDelay: 10,
});

expect(result).toBe("success");
expect(mockFn).toHaveBeenCalledTimes(2);
});

it("should retry on socket hang up error", async () => {
const error = new Error("socket hang up");
const mockFn = jest
.fn()
.mockRejectedValueOnce(error)
.mockResolvedValue("success");

const result = await retryAsync(mockFn, {
maxAttempts: 2,
baseDelay: 10,
});

expect(result).toBe("success");
expect(mockFn).toHaveBeenCalledTimes(2);
});

it("should retry on premature end error", async () => {
const error = new Error("premature end of stream");
const mockFn = jest
.fn()
.mockRejectedValueOnce(error)
.mockResolvedValue("success");

const result = await retryAsync(mockFn, {
maxAttempts: 2,
baseDelay: 10,
});

expect(result).toBe("success");
expect(mockFn).toHaveBeenCalledTimes(2);
});

it("should retry on connection reset message error", async () => {
const error = new Error("connection reset by peer");
const mockFn = jest
.fn()
.mockRejectedValueOnce(error)
.mockResolvedValue("success");

const result = await retryAsync(mockFn, {
maxAttempts: 2,
baseDelay: 10,
});

expect(result).toBe("success");
expect(mockFn).toHaveBeenCalledTimes(2);
});

it("should not retry AbortError", async () => {
const error = new Error("Aborted");
error.name = "AbortError";
Expand Down
11 changes: 11 additions & 0 deletions core/llm/utils/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ function defaultShouldRetry(error: any, attempt: number): boolean {
return true;
}

// Connection/stream errors (e.g. provider drops connection mid-stream)
const connectionPatterns = [
"premature close",
"premature end",
"connection reset",
"socket hang up",
];
if (connectionPatterns.some((p) => lowerMessage.includes(p))) {
return true;
}

// Abort signal errors should not be retried
if (isAbortError(error)) {
return false;
Expand Down
6 changes: 5 additions & 1 deletion core/util/withExponentialBackoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ const withExponentialBackoff = async <T>(
(error as APIError).response?.status === 429 ||
/"code"\s*:\s*429/.test(error.message ?? "") ||
lowerMessage.includes("overloaded") ||
lowerMessage.includes("malformed json")
lowerMessage.includes("malformed json") ||
lowerMessage.includes("premature close") ||
lowerMessage.includes("premature end") ||
lowerMessage.includes("connection reset") ||
lowerMessage.includes("socket hang up")
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 25, 2026

Choose a reason for hiding this comment

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

P3: The new retryable connection errors are logged as "Hit rate limit," which is inaccurate and can mislead troubleshooting. Use a generic retry message (or include the actual error type) for this shared retry branch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At core/util/withExponentialBackoff.ts, line 26:

<comment>The new retryable connection errors are logged as "Hit rate limit," which is inaccurate and can mislead troubleshooting. Use a generic retry message (or include the actual error type) for this shared retry branch.</comment>

<file context>
@@ -19,7 +19,11 @@ const withExponentialBackoff = async <T>(
+        lowerMessage.includes("premature close") ||
+        lowerMessage.includes("premature end") ||
+        lowerMessage.includes("connection reset") ||
+        lowerMessage.includes("socket hang up")
       ) {
         const retryAfter = (error as APIError).response?.headers.get(
</file context>
Fix with Cubic

) {
const retryAfter = (error as APIError).response?.headers.get(
RETRY_AFTER_HEADER,
Expand Down
Loading