diff options
| author | André Cruz <[email protected]> | 2026-03-25 18:08:40 +0000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-25 13:08:40 -0500 |
| commit | 7123aad5a8c8957ee5ae34a0d82c9e6800f7109e (patch) | |
| tree | a4905d2e57e5c0dedfd94285f16832b364bf3243 | |
| parent | d6fc5f414b1f78994fffd550d4104627dbbfac51 (diff) | |
| download | opencode-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.ts | 27 | ||||
| -rw-r--r-- | packages/opencode/src/session/processor.ts | 2 | ||||
| -rw-r--r-- | packages/opencode/test/session/message-v2.test.ts | 27 | ||||
| -rw-r--r-- | packages/opencode/test/session/retry.test.ts | 12 |
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", () => { |
