fix(sdk+cli): surface real errors instead of bare {} when server returns empty body (#25592)

This commit is contained in:
Kit Langton 2026-05-03 09:17:06 -04:00 committed by GitHub
parent 7a503de606
commit 379600b5ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 49 additions and 10 deletions

View file

@ -7,7 +7,19 @@ export function errorFormat(error: unknown): string {
if (typeof error === "object" && error !== null) {
try {
return JSON.stringify(error, null, 2)
const json = JSON.stringify(error, null, 2)
// Plain objects whose own properties are all non-enumerable (or empty)
// serialize to "{}", which prints as a useless bare `{}` on stderr.
// Fall back to a custom toString first, then to ctor name + own prop names.
if (json === "{}") {
const str = String(error)
if (str && str !== "[object Object]") return str
const ctor = error.constructor?.name
const prefix = ctor && ctor !== "Object" ? ctor : "Error"
const names = Object.getOwnPropertyNames(error)
return names.length === 0 ? `${prefix} (no message)` : `${prefix} { ${names.join(", ")} }`
}
return json
} catch {
return "Unexpected error (unserializable)"
}
@ -34,7 +46,7 @@ export function errorMessage(error: unknown): string {
if (text && text !== "[object Object]") return text
const formatted = errorFormat(error)
if (formatted && formatted !== "{}") return formatted
if (formatted) return formatted
return "unknown error"
}
@ -45,7 +57,7 @@ export function errorData(error: unknown) {
message: errorMessage(error),
stack: error.stack,
cause: error.cause === undefined ? undefined : errorFormat(error.cause),
formatted: errorFormatted(error),
formatted: errorFormat(error),
}
}
@ -53,7 +65,7 @@ export function errorData(error: unknown) {
return {
type: typeof error,
message: errorMessage(error),
formatted: errorFormatted(error),
formatted: errorFormat(error),
}
}
@ -71,12 +83,7 @@ export function errorData(error: unknown) {
if (typeof data.message !== "string") data.message = errorMessage(error)
if (typeof data.type !== "string") data.type = error.constructor?.name
data.formatted = errorFormatted(error)
data.formatted = errorFormat(error)
return data
}
function errorFormatted(error: unknown) {
const formatted = errorFormat(error)
if (formatted !== "{}") return formatted
return String(error)
}

View file

@ -22,6 +22,19 @@ describe("util.error", () => {
expect(data.code).toBe("E_BAD")
})
test("never returns bare {} for opaque object errors", () => {
// Plain empty object — what the SDK threw before we wrapped it.
expect(errorFormat({})).not.toBe("{}")
expect(errorFormat({})).toContain("no message")
// Object with only non-enumerable own properties (JSON.stringify drops them).
class OpaqueError {}
const opaque = new OpaqueError()
Object.defineProperty(opaque, "secret", { value: "hidden", enumerable: false })
expect(errorFormat(opaque)).not.toBe("{}")
expect(errorFormat(opaque)).toContain("OpaqueError")
})
test("handles opaque throwables with custom toString", () => {
const err = {
toString() {

View file

@ -84,5 +84,24 @@ export function createOpencodeClient(config?: Config & { directory?: string; exp
return response
})
// The generated client falls back to throwing a literal `{}` when the server
// responds with an empty / unparseable error body, which surfaces as a bare
// `{}` in TUI / CLI error output. Wrap ONLY that case in a real Error so
// downstream formatters get a useful message — but pass through any parsed
// JSON error body unchanged so existing consumers can still inspect fields.
client.interceptors.error.use((error, response, request) => {
const isEmpty =
error === undefined ||
error === null ||
error === "" ||
(typeof error === "object" && !(error instanceof Error) && Object.keys(error).length === 0)
if (!isEmpty) return error
const method = request?.method ?? "?"
const url = request?.url ?? "?"
if (!response) return new Error(`opencode server ${method} ${url}: network error (no response)`)
const status = response.status
const statusText = response.statusText ? " " + response.statusText : ""
return new Error(`opencode server ${method} ${url}${status}${statusText}: (empty response body)`)
})
return new OpencodeClient({ client })
}