summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrĂ© Cruz <[email protected]>2026-03-25 18:08:40 +0000
committerGitHub <[email protected]>2026-03-25 13:08:40 -0500
commit7123aad5a8c8957ee5ae34a0d82c9e6800f7109e (patch)
treea4905d2e57e5c0dedfd94285f16832b364bf3243
parentd6fc5f414b1f78994fffd550d4104627dbbfac51 (diff)
downloadopencode-7123aad5a8c8957ee5ae34a0d82c9e6800f7109e.tar.gz
opencode-7123aad5a8c8957ee5ae34a0d82c9e6800f7109e.zip
fix(opencode): classify ZlibError from Bun fetch as retryable instead of unknown (#19104)
Co-authored-by: Aiden Cline <[email protected]>
-rw-r--r--packages/opencode/src/session/message-v2.ts27
-rw-r--r--packages/opencode/src/session/processor.ts2
-rw-r--r--packages/opencode/test/session/message-v2.test.ts27
-rw-r--r--packages/opencode/test/session/retry.test.ts12
4 files changed, 66 insertions, 2 deletions
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index d90910650..86e431565 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -15,6 +15,13 @@ import type { SystemError } from "bun"
import type { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "@/provider/schema"
+/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
+interface FetchDecompressionError extends Error {
+ code: "ZlibError"
+ errno: number
+ path: string
+}
+
export namespace MessageV2 {
export function isMedia(mime: string) {
return mime.startsWith("image/") || mime === "application/pdf"
@@ -906,7 +913,10 @@ export namespace MessageV2 {
return result
}
- export function fromError(e: unknown, ctx: { providerID: ProviderID }): NonNullable<Assistant["error"]> {
+ export function fromError(
+ e: unknown,
+ ctx: { providerID: ProviderID; aborted?: boolean },
+ ): NonNullable<Assistant["error"]> {
switch (true) {
case e instanceof DOMException && e.name === "AbortError":
return new MessageV2.AbortedError(
@@ -938,6 +948,21 @@ export namespace MessageV2 {
},
{ cause: e },
).toObject()
+ case e instanceof Error && (e as FetchDecompressionError).code === "ZlibError":
+ if (ctx.aborted) {
+ return new MessageV2.AbortedError({ message: e.message }, { cause: e }).toObject()
+ }
+ return new MessageV2.APIError(
+ {
+ message: "Response decompression failed",
+ isRetryable: true,
+ metadata: {
+ code: (e as FetchDecompressionError).code,
+ message: e.message,
+ },
+ },
+ { cause: e },
+ ).toObject()
case APICallError.isInstance(e):
const parsed = ProviderError.parseAPICallError({
providerID: ctx.providerID,
diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts
index ccb09e71a..84ea76656 100644
--- a/packages/opencode/src/session/processor.ts
+++ b/packages/opencode/src/session/processor.ts
@@ -356,7 +356,7 @@ export namespace SessionProcessor {
error: e,
stack: JSON.stringify(e.stack),
})
- const error = MessageV2.fromError(e, { providerID: input.model.providerID })
+ const error = MessageV2.fromError(e, { providerID: input.model.providerID, aborted: input.abort.aborted })
if (MessageV2.ContextOverflowError.isInstance(error)) {
needsCompaction = true
Bus.publish(Session.Event.Error, {
diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts
index 0d5b89730..7d416597a 100644
--- a/packages/opencode/test/session/message-v2.test.ts
+++ b/packages/opencode/test/session/message-v2.test.ts
@@ -927,4 +927,31 @@ describe("session.message-v2.fromError", () => {
},
})
})
+
+ test("classifies ZlibError from fetch as retryable APIError", () => {
+ const zlibError = new Error(
+ 'ZlibError fetching "https://opencode.cloudflare.dev/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
+ )
+ ;(zlibError as any).code = "ZlibError"
+ ;(zlibError as any).errno = 0
+ ;(zlibError as any).path = ""
+
+ const result = MessageV2.fromError(zlibError, { providerID })
+
+ expect(MessageV2.APIError.isInstance(result)).toBe(true)
+ expect((result as MessageV2.APIError).data.isRetryable).toBe(true)
+ expect((result as MessageV2.APIError).data.message).toInclude("decompression")
+ })
+
+ test("classifies ZlibError as AbortedError when abort context is provided", () => {
+ const zlibError = new Error(
+ 'ZlibError fetching "https://opencode.cloudflare.dev/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
+ )
+ ;(zlibError as any).code = "ZlibError"
+ ;(zlibError as any).errno = 0
+
+ const result = MessageV2.fromError(zlibError, { providerID, aborted: true })
+
+ expect(result.name).toBe("MessageAbortedError")
+ })
})
diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts
index 621ad99e9..a61c44262 100644
--- a/packages/opencode/test/session/retry.test.ts
+++ b/packages/opencode/test/session/retry.test.ts
@@ -125,6 +125,18 @@ describe("session.retry.retryable", () => {
expect(SessionRetry.retryable(error)).toBeUndefined()
})
+
+ test("retries ZlibError decompression failures", () => {
+ const error = new MessageV2.APIError({
+ message: "Response decompression failed",
+ isRetryable: true,
+ metadata: { code: "ZlibError" },
+ }).toObject() as MessageV2.APIError
+
+ const retryable = SessionRetry.retryable(error)
+ expect(retryable).toBeDefined()
+ expect(retryable).toBe("Response decompression failed")
+ })
})
describe("session.message-v2.fromError", () => {