Skip to content

Commit 4ca809e

Browse files
authored
fix(session): retry 5xx server errors even when isRetryable is unset (#22511)
1 parent a147ad6 commit 4ca809e

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

packages/opencode/src/session/retry.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ export namespace SessionRetry {
5656
// context overflow errors should not be retried
5757
if (MessageV2.ContextOverflowError.isInstance(error)) return undefined
5858
if (MessageV2.APIError.isInstance(error)) {
59-
if (!error.data.isRetryable) return undefined
59+
const status = error.data.statusCode
60+
// 5xx errors are transient server failures and should always be retried,
61+
// even when the provider SDK doesn't explicitly mark them as retryable.
62+
if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined
6063
if (error.data.responseBody?.includes("FreeUsageLimitError")) return GO_UPSELL_MESSAGE
6164
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
6265
}

packages/opencode/test/session/retry.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,47 @@ describe("session.retry.retryable", () => {
178178
expect(SessionRetry.retryable(error)).toBeUndefined()
179179
})
180180

181+
test("retries 500 errors even when isRetryable is false", () => {
182+
const error = new MessageV2.APIError({
183+
message: "Internal server error",
184+
isRetryable: false,
185+
statusCode: 500,
186+
responseBody: '{"type":"api_error","message":"Internal server error"}',
187+
}).toObject() as MessageV2.APIError
188+
189+
expect(SessionRetry.retryable(error)).toBe("Internal server error")
190+
})
191+
192+
test("retries 502 bad gateway errors", () => {
193+
const error = new MessageV2.APIError({
194+
message: "Bad gateway",
195+
isRetryable: false,
196+
statusCode: 502,
197+
}).toObject() as MessageV2.APIError
198+
199+
expect(SessionRetry.retryable(error)).toBe("Bad gateway")
200+
})
201+
202+
test("retries 503 service unavailable errors", () => {
203+
const error = new MessageV2.APIError({
204+
message: "Service unavailable",
205+
isRetryable: false,
206+
statusCode: 503,
207+
}).toObject() as MessageV2.APIError
208+
209+
expect(SessionRetry.retryable(error)).toBe("Service unavailable")
210+
})
211+
212+
test("does not retry 4xx errors when isRetryable is false", () => {
213+
const error = new MessageV2.APIError({
214+
message: "Bad request",
215+
isRetryable: false,
216+
statusCode: 400,
217+
}).toObject() as MessageV2.APIError
218+
219+
expect(SessionRetry.retryable(error)).toBeUndefined()
220+
})
221+
181222
test("retries ZlibError decompression failures", () => {
182223
const error = new MessageV2.APIError({
183224
message: "Response decompression failed",

0 commit comments

Comments
 (0)