Skip to content

Commit 379600b

Browse files
authored
fix(sdk+cli): surface real errors instead of bare {} when server returns empty body (#25592)
1 parent 7a503de commit 379600b

3 files changed

Lines changed: 49 additions & 10 deletions

File tree

packages/opencode/src/util/error.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,19 @@ export function errorFormat(error: unknown): string {
77

88
if (typeof error === "object" && error !== null) {
99
try {
10-
return JSON.stringify(error, null, 2)
10+
const json = JSON.stringify(error, null, 2)
11+
// Plain objects whose own properties are all non-enumerable (or empty)
12+
// serialize to "{}", which prints as a useless bare `{}` on stderr.
13+
// Fall back to a custom toString first, then to ctor name + own prop names.
14+
if (json === "{}") {
15+
const str = String(error)
16+
if (str && str !== "[object Object]") return str
17+
const ctor = error.constructor?.name
18+
const prefix = ctor && ctor !== "Object" ? ctor : "Error"
19+
const names = Object.getOwnPropertyNames(error)
20+
return names.length === 0 ? `${prefix} (no message)` : `${prefix} { ${names.join(", ")} }`
21+
}
22+
return json
1123
} catch {
1224
return "Unexpected error (unserializable)"
1325
}
@@ -34,7 +46,7 @@ export function errorMessage(error: unknown): string {
3446
if (text && text !== "[object Object]") return text
3547

3648
const formatted = errorFormat(error)
37-
if (formatted && formatted !== "{}") return formatted
49+
if (formatted) return formatted
3850
return "unknown error"
3951
}
4052

@@ -45,15 +57,15 @@ export function errorData(error: unknown) {
4557
message: errorMessage(error),
4658
stack: error.stack,
4759
cause: error.cause === undefined ? undefined : errorFormat(error.cause),
48-
formatted: errorFormatted(error),
60+
formatted: errorFormat(error),
4961
}
5062
}
5163

5264
if (!isRecord(error)) {
5365
return {
5466
type: typeof error,
5567
message: errorMessage(error),
56-
formatted: errorFormatted(error),
68+
formatted: errorFormat(error),
5769
}
5870
}
5971

@@ -71,12 +83,7 @@ export function errorData(error: unknown) {
7183

7284
if (typeof data.message !== "string") data.message = errorMessage(error)
7385
if (typeof data.type !== "string") data.type = error.constructor?.name
74-
data.formatted = errorFormatted(error)
86+
data.formatted = errorFormat(error)
7587
return data
7688
}
7789

78-
function errorFormatted(error: unknown) {
79-
const formatted = errorFormat(error)
80-
if (formatted !== "{}") return formatted
81-
return String(error)
82-
}

packages/opencode/test/util/error.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ describe("util.error", () => {
2222
expect(data.code).toBe("E_BAD")
2323
})
2424

25+
test("never returns bare {} for opaque object errors", () => {
26+
// Plain empty object — what the SDK threw before we wrapped it.
27+
expect(errorFormat({})).not.toBe("{}")
28+
expect(errorFormat({})).toContain("no message")
29+
30+
// Object with only non-enumerable own properties (JSON.stringify drops them).
31+
class OpaqueError {}
32+
const opaque = new OpaqueError()
33+
Object.defineProperty(opaque, "secret", { value: "hidden", enumerable: false })
34+
expect(errorFormat(opaque)).not.toBe("{}")
35+
expect(errorFormat(opaque)).toContain("OpaqueError")
36+
})
37+
2538
test("handles opaque throwables with custom toString", () => {
2639
const err = {
2740
toString() {

packages/sdk/js/src/v2/client.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,24 @@ export function createOpencodeClient(config?: Config & { directory?: string; exp
8484

8585
return response
8686
})
87+
// The generated client falls back to throwing a literal `{}` when the server
88+
// responds with an empty / unparseable error body, which surfaces as a bare
89+
// `{}` in TUI / CLI error output. Wrap ONLY that case in a real Error so
90+
// downstream formatters get a useful message — but pass through any parsed
91+
// JSON error body unchanged so existing consumers can still inspect fields.
92+
client.interceptors.error.use((error, response, request) => {
93+
const isEmpty =
94+
error === undefined ||
95+
error === null ||
96+
error === "" ||
97+
(typeof error === "object" && !(error instanceof Error) && Object.keys(error).length === 0)
98+
if (!isEmpty) return error
99+
const method = request?.method ?? "?"
100+
const url = request?.url ?? "?"
101+
if (!response) return new Error(`opencode server ${method} ${url}: network error (no response)`)
102+
const status = response.status
103+
const statusText = response.statusText ? " " + response.statusText : ""
104+
return new Error(`opencode server ${method} ${url}${status}${statusText}: (empty response body)`)
105+
})
87106
return new OpencodeClient({ client })
88107
}

0 commit comments

Comments
 (0)