summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-15 20:58:48 -0400
committerGitHub <[email protected]>2026-04-16 00:58:48 +0000
commit4ca809ef4e71ee6d62990c815c82c7ee57395a8b (patch)
treefc8f0d364c35757cc9c737f068b6aec71cfaf0e6
parenta147ad68e6aed8a6a3eeaf2ce1e56f73fab7fa31 (diff)
downloadopencode-4ca809ef4e71ee6d62990c815c82c7ee57395a8b.tar.gz
opencode-4ca809ef4e71ee6d62990c815c82c7ee57395a8b.zip
fix(session): retry 5xx server errors even when isRetryable is unset (#22511)
-rw-r--r--packages/opencode/src/session/retry.ts5
-rw-r--r--packages/opencode/test/session/retry.test.ts41
2 files changed, 45 insertions, 1 deletions
diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts
index 39eb8cfb7..6aad55f3f 100644
--- a/packages/opencode/src/session/retry.ts
+++ b/packages/opencode/src/session/retry.ts
@@ -56,7 +56,10 @@ export namespace SessionRetry {
// context overflow errors should not be retried
if (MessageV2.ContextOverflowError.isInstance(error)) return undefined
if (MessageV2.APIError.isInstance(error)) {
- if (!error.data.isRetryable) return undefined
+ const status = error.data.statusCode
+ // 5xx errors are transient server failures and should always be retried,
+ // even when the provider SDK doesn't explicitly mark them as retryable.
+ if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined
if (error.data.responseBody?.includes("FreeUsageLimitError")) return GO_UPSELL_MESSAGE
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
}
diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts
index 314306ba6..2d01a8f35 100644
--- a/packages/opencode/test/session/retry.test.ts
+++ b/packages/opencode/test/session/retry.test.ts
@@ -178,6 +178,47 @@ describe("session.retry.retryable", () => {
expect(SessionRetry.retryable(error)).toBeUndefined()
})
+ test("retries 500 errors even when isRetryable is false", () => {
+ const error = new MessageV2.APIError({
+ message: "Internal server error",
+ isRetryable: false,
+ statusCode: 500,
+ responseBody: '{"type":"api_error","message":"Internal server error"}',
+ }).toObject() as MessageV2.APIError
+
+ expect(SessionRetry.retryable(error)).toBe("Internal server error")
+ })
+
+ test("retries 502 bad gateway errors", () => {
+ const error = new MessageV2.APIError({
+ message: "Bad gateway",
+ isRetryable: false,
+ statusCode: 502,
+ }).toObject() as MessageV2.APIError
+
+ expect(SessionRetry.retryable(error)).toBe("Bad gateway")
+ })
+
+ test("retries 503 service unavailable errors", () => {
+ const error = new MessageV2.APIError({
+ message: "Service unavailable",
+ isRetryable: false,
+ statusCode: 503,
+ }).toObject() as MessageV2.APIError
+
+ expect(SessionRetry.retryable(error)).toBe("Service unavailable")
+ })
+
+ test("does not retry 4xx errors when isRetryable is false", () => {
+ const error = new MessageV2.APIError({
+ message: "Bad request",
+ isRetryable: false,
+ statusCode: 400,
+ }).toObject() as MessageV2.APIError
+
+ expect(SessionRetry.retryable(error)).toBeUndefined()
+ })
+
test("retries ZlibError decompression failures", () => {
const error = new MessageV2.APIError({
message: "Response decompression failed",