Skip to content

Commit e760eb9

Browse files
edevilrekram1-node
authored andcommitted
fix(opencode): classify ZlibError from Bun fetch as retryable instead of unknown (anomalyco#19104)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
1 parent cdcd1d9 commit e760eb9

4 files changed

Lines changed: 66 additions & 2 deletions

File tree

packages/opencode/src/session/message-v2.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ import type { SystemError } from "bun"
1515
import type { Provider } from "@/provider/provider"
1616
import { ModelID, ProviderID } from "@/provider/schema"
1717

18+
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
19+
interface FetchDecompressionError extends Error {
20+
code: "ZlibError"
21+
errno: number
22+
path: string
23+
}
24+
1825
export namespace MessageV2 {
1926
export function isMedia(mime: string) {
2027
return mime.startsWith("image/") || mime === "application/pdf"
@@ -906,7 +913,10 @@ export namespace MessageV2 {
906913
return result
907914
}
908915

909-
export function fromError(e: unknown, ctx: { providerID: ProviderID }): NonNullable<Assistant["error"]> {
916+
export function fromError(
917+
e: unknown,
918+
ctx: { providerID: ProviderID; aborted?: boolean },
919+
): NonNullable<Assistant["error"]> {
910920
switch (true) {
911921
case e instanceof DOMException && e.name === "AbortError":
912922
return new MessageV2.AbortedError(
@@ -938,6 +948,21 @@ export namespace MessageV2 {
938948
},
939949
{ cause: e },
940950
).toObject()
951+
case e instanceof Error && (e as FetchDecompressionError).code === "ZlibError":
952+
if (ctx.aborted) {
953+
return new MessageV2.AbortedError({ message: e.message }, { cause: e }).toObject()
954+
}
955+
return new MessageV2.APIError(
956+
{
957+
message: "Response decompression failed",
958+
isRetryable: true,
959+
metadata: {
960+
code: (e as FetchDecompressionError).code,
961+
message: e.message,
962+
},
963+
},
964+
{ cause: e },
965+
).toObject()
941966
case APICallError.isInstance(e):
942967
const parsed = ProviderError.parseAPICallError({
943968
providerID: ctx.providerID,

packages/opencode/src/session/processor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ export namespace SessionProcessor {
356356
error: e,
357357
stack: JSON.stringify(e.stack),
358358
})
359-
const error = MessageV2.fromError(e, { providerID: input.model.providerID })
359+
const error = MessageV2.fromError(e, { providerID: input.model.providerID, aborted: input.abort.aborted })
360360
if (MessageV2.ContextOverflowError.isInstance(error)) {
361361
needsCompaction = true
362362
Bus.publish(Session.Event.Error, {

packages/opencode/test/session/message-v2.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,4 +927,31 @@ describe("session.message-v2.fromError", () => {
927927
},
928928
})
929929
})
930+
931+
test("classifies ZlibError from fetch as retryable APIError", () => {
932+
const zlibError = new Error(
933+
'ZlibError fetching "https://opencode.cloudflare.dev/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
934+
)
935+
;(zlibError as any).code = "ZlibError"
936+
;(zlibError as any).errno = 0
937+
;(zlibError as any).path = ""
938+
939+
const result = MessageV2.fromError(zlibError, { providerID })
940+
941+
expect(MessageV2.APIError.isInstance(result)).toBe(true)
942+
expect((result as MessageV2.APIError).data.isRetryable).toBe(true)
943+
expect((result as MessageV2.APIError).data.message).toInclude("decompression")
944+
})
945+
946+
test("classifies ZlibError as AbortedError when abort context is provided", () => {
947+
const zlibError = new Error(
948+
'ZlibError fetching "https://opencode.cloudflare.dev/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
949+
)
950+
;(zlibError as any).code = "ZlibError"
951+
;(zlibError as any).errno = 0
952+
953+
const result = MessageV2.fromError(zlibError, { providerID, aborted: true })
954+
955+
expect(result.name).toBe("MessageAbortedError")
956+
})
930957
})

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,18 @@ describe("session.retry.retryable", () => {
125125

126126
expect(SessionRetry.retryable(error)).toBeUndefined()
127127
})
128+
129+
test("retries ZlibError decompression failures", () => {
130+
const error = new MessageV2.APIError({
131+
message: "Response decompression failed",
132+
isRetryable: true,
133+
metadata: { code: "ZlibError" },
134+
}).toObject() as MessageV2.APIError
135+
136+
const retryable = SessionRetry.retryable(error)
137+
expect(retryable).toBeDefined()
138+
expect(retryable).toBe("Response decompression failed")
139+
})
128140
})
129141

130142
describe("session.message-v2.fromError", () => {

0 commit comments

Comments
 (0)