Skip to content

🤖 fix: show provider error details in AI debug logs#2791

Merged
ThomasK33 merged 1 commit intomainfrom
debug-logs-2pjd
Mar 4, 2026
Merged

🤖 fix: show provider error details in AI debug logs#2791
ThomasK33 merged 1 commit intomainfrom
debug-logs-2pjd

Conversation

@ThomasK33
Copy link
Member

Summary

Fix AI Debug Logs error rendering so provider object payloads no longer appear as Error: [object Object].

Background

Some providers return structured error objects rather than Error instances. The middleware's local helper and the shared utility previously fell back to String(error) for non-Error values, collapsing useful payloads into [object Object].

Implementation

  • Enhanced getErrorMessage to:
    • Prefer non-empty message from plain objects
    • Fallback to JSON.stringify for object/array payloads
    • Gracefully fallback to String(error) when serialization fails (e.g. circular refs)
  • Added regression tests covering plain objects, arrays, and circular object fallback.
  • Removed duplicate extractErrorMessage from devToolsMiddleware and reused shared getErrorMessage at all middleware callsites.

Validation

  • bun test src/common/utils/errors.test.ts
  • bun test src/node/services/__tests__/devToolsMiddleware.test.ts
  • make typecheck
  • make static-check

Risks

Low risk. The change is scoped to error-message normalization and covered by targeted tests plus existing middleware tests.


📋 Implementation Plan

Fix "Error: [object Object]" in AI Debug Logs

Context

The DevTools debug logs panel shows Error: [object Object] instead of the actual error message when an AI provider returns an error. LLM providers return plain objects (e.g. { message: "rate limit exceeded", code: 429 }), not Error instances, and two utility functions naively call String(error) on them — producing "[object Object]".

Root Cause

Two functions with the same bug:

  1. extractErrorMessage in src/node/services/devToolsMiddleware.ts:30–36 — private helper, 4 call sites within the file (lines 433, 565, 640, 683). Direct cause of the screenshot.
  2. getErrorMessage in src/common/utils/errors.ts:8–9 — shared utility, ~387 call sites app-wide. Same String(error) fallback for non-Error values.

extractErrorMessage is strictly a worse duplicate of getErrorMessage (no cause-chain walking). After fixing getErrorMessage, we can eliminate extractErrorMessage entirely.


Phase 1 — Red: Write Failing Tests

File: src/common/utils/errors.test.ts

Add test cases that capture the exact behavior we want. These will fail against the current String(error) fallback:

it("returns .message from a plain object with a message property", () => {
  expect(getErrorMessage({ message: "rate limit exceeded", code: 429 })).toBe(
    "rate limit exceeded",
  );
});

it("returns JSON for a plain object without a message property", () => {
  expect(getErrorMessage({ code: 429, status: "error" })).toBe(
    '{"code":429,"status":"error"}',
  );
});

it("returns JSON for a plain object with empty message", () => {
  expect(getErrorMessage({ message: "", code: 500 })).toBe(
    '{"message":"","code":500}',
  );
});

it("returns JSON for an array error value", () => {
  expect(getErrorMessage(["error1", "error2"])).toBe('["error1","error2"]');
});

it("falls back to String() for circular plain objects", () => {
  const obj: Record<string, unknown> = { code: 500 };
  obj.self = obj;
  // JSON.stringify would throw, so we must fall back gracefully
  expect(getErrorMessage(obj)).toBe("[object Object]");
});

Verify: bun test src/common/utils/errors.test.ts — expect 3+ failures (plain object, JSON, array tests fail with "[object Object]" instead of expected values).

Phase 2 — Green: Fix getErrorMessage

File: src/common/utils/errors.ts — replace the early return for non-Error values (~8 LoC net).

export function getErrorMessage(error: unknown): string {
  if (!(error instanceof Error)) {
    // Plain objects with a .message string (common for LLM provider errors)
    if (typeof error === "object" && error !== null) {
      if (
        "message" in error &&
        typeof (error as Record<string, unknown>).message === "string" &&
        ((error as Record<string, unknown>).message as string).length > 0
      ) {
        return (error as Record<string, unknown>).message as string;
      }
      try {
        return JSON.stringify(error);
      } catch {
        // Circular references, etc. — fall through to String()
      }
    }
    return String(error);
  }
  // ... existing cause-chain walking unchanged ...
}

Key behaviors:

  • { message: "rate limit" }"rate limit" (extract .message)
  • { code: 429 }'{"code":429}' (JSON stringify for objects without .message)
  • { message: "", code: 500 }'{"message":"","code":500}' (empty .message treated as absent)
  • Circular objects → "[object Object]" (graceful fallback via try/catch around JSON.stringify)
  • Primitives like "boom", 42, null → unchanged (String())

Verify: bun test src/common/utils/errors.test.ts — all tests pass, including the existing ones (no regressions on Error instances, cause chains, etc.).

Phase 3 — Refactor: Eliminate extractErrorMessage

extractErrorMessage in devToolsMiddleware.ts is now a strictly worse duplicate. Replace all 4 call sites with the shared utility and delete the private function.

File: src/node/services/devToolsMiddleware.ts

  1. Add import at top: import { getErrorMessage } from "@/common/utils/errors";
  2. Delete extractErrorMessage function (lines 30–36, ~7 LoC removed)
  3. Replace 4 call sites:
    • Line 433: error: extractErrorMessage(error)error: getErrorMessage(error)
    • Line 565: same
    • Line 640: streamError = extractErrorMessage(chunk.error)streamError = getErrorMessage(chunk.error)
    • Line 683: same as 433

Verify:

  • bun test src/common/utils/errors.test.ts — still green
  • bun test src/node/services/__tests__/devToolsMiddleware.test.ts — no regressions
  • make typecheck — clean

Net LoC: ~+15 (product: ~+5, tests: ~+20)

Adds ~12 lines to getErrorMessage + tests, removes ~7 lines (extractErrorMessage + its call-site differences).


Generated with mux • Model: openai:gpt-5.3-codex • Thinking: xhigh • Cost: $1.95

@ThomasK33
Copy link
Member Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ef8586ffa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33
Copy link
Member Author

@codex review

Addressed your feedback by ensuring getErrorMessage falls back to String(error) when JSON.stringify returns undefined, and added a regression test for that path.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87da28c814

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Handle provider object errors via getErrorMessage, add regression tests, and remove duplicate middleware helper.

---

_Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$1.95`_

<!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=1.95 -->
@ThomasK33
Copy link
Member Author

@codex review

Applied your follow-up suggestion:

  • Wrapped non-Error message property access in the same try block as serialization to keep getErrorMessage non-throwing for throwing getters/proxies.
  • Added regression coverage for an object with a throwing message getter.

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Keep it up!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33 ThomasK33 added this pull request to the merge queue Mar 4, 2026
Merged via the queue into main with commit dc99a0b Mar 4, 2026
38 of 40 checks passed
@ThomasK33 ThomasK33 deleted the debug-logs-2pjd branch March 4, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant