summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-05-03 09:17:06 -0400
committerGitHub <[email protected]>2026-05-03 13:17:06 +0000
commit379600b5ab9ed46043d1674e7fb7c3dbcb9bd4ba (patch)
treeb69e91e9649f53a2a780c86c12c994e44d43f2ed
parent7a503de606888939a64776c512ca4588267bbd8d (diff)
downloadopencode-379600b5ab9ed46043d1674e7fb7c3dbcb9bd4ba.tar.gz
opencode-379600b5ab9ed46043d1674e7fb7c3dbcb9bd4ba.zip
fix(sdk+cli): surface real errors instead of bare {} when server returns empty body (#25592)
-rw-r--r--packages/opencode/src/util/error.ts27
-rw-r--r--packages/opencode/test/util/error.test.ts13
-rw-r--r--packages/sdk/js/src/v2/client.ts19
3 files changed, 49 insertions, 10 deletions
diff --git a/packages/opencode/src/util/error.ts b/packages/opencode/src/util/error.ts
index fbda2dc50..32936e993 100644
--- a/packages/opencode/src/util/error.ts
+++ b/packages/opencode/src/util/error.ts
@@ -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)
-}
diff --git a/packages/opencode/test/util/error.test.ts b/packages/opencode/test/util/error.test.ts
index e536f3c4e..e7a02d615 100644
--- a/packages/opencode/test/util/error.test.ts
+++ b/packages/opencode/test/util/error.test.ts
@@ -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() {
diff --git a/packages/sdk/js/src/v2/client.ts b/packages/sdk/js/src/v2/client.ts
index 2d71d8446..8b49e7f10 100644
--- a/packages/sdk/js/src/v2/client.ts
+++ b/packages/sdk/js/src/v2/client.ts
@@ -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 })
}