Ver Fonte

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

Kit Langton há 4 meses atrás
pai
commit
379600b5ab

+ 17 - 10
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)
-}

+ 13 - 0
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() {

+ 19 - 0
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 })
 }