fix(sdk): wrap thrown error bodies in real Error instances - #1139
Conversation
The v2 client's error interceptor only wrapped empty/unparseable bodies.
Non-empty structured bodies (opencode 4xx NamedError responses, e.g.
`{ data: { message } }`) passed straight through, so the `throwOnError`
path threw a bare POJO. Downstream formatters that read `instanceof Error`
or `.message` (CLI `run`, ACP, plugins, TUI) then surfaced `[object Object]`
or an empty message instead of the server's error text.
Extract the interceptor into an exported `wrapClientError` and convert
non-empty bodies into real `Error`s on the `throwOnError` path, preserving
the original parsed body and status under `.cause`. The result-tuple path
(`result.error`) still returns the raw body untouched, so field-level reads
stay byte-for-byte identical, and empty/network failures keep their existing
descriptive message on both paths.
Ports the idea from upstream anomalyco/opencode 11363170ca, adapted to
PawWork's diverged SDK: scoped to the active v2 client only, with the helper
inlined (one file) rather than a shared module, and empty-body wrapping kept
unconditional to avoid regressing `result.error` message quality.
Test: red->green in v2-client-error-interceptor.test.ts — a structured
`{ data: { message } }` body now throws a real Error with the extracted
message; pure-function branch coverage plus end-to-end coverage through
`createOpencodeClient`'s real gen error pipeline.
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a new wrapClientError utility function in the JS SDK client to properly wrap non-2xx error bodies into standard Error objects. This ensures downstream formatters receive a meaningful error message instead of raw objects, while still preserving the original structured body and status under the cause property. The client's error interceptor has been updated to use this utility, and comprehensive unit tests have been added to verify various error scenarios. I have no feedback to provide as there are no review comments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Restructure the right-side Status panel from 2 sections (Progress + Sources) into 4 (Progress -> Workspace/Git -> Changed files/Artifact -> Sources), fold the standalone Files tab into the panel as a compact changed-files list, and move the titlebar worktree badge into the Git section. Change boundary (packages/app, packages/ui): - Status panel: new Git section (diff stats -> Review, branch row, worktree indicator with tooltip + open-directory) and Artifact section (changed files with hover/focus-visible open + reveal); Git section hidden outside a git repo. Data from sync.data.vcs (branch), sessionInfo().executionContext.activeWorktree (worktree), aggregateFiles(turn_change_aggregate) (diff stats). - Files tab removed from RightPanelStaticTab / RIGHT_PANEL_TAB_META / command palette / keybind; legacy persisted "files" coerced to "status" via coerceLegacySidePanelTab + migrateLegacyRightPanelTab. - Titlebar PawworkWorktreeBadge removed (worktree info now lives in the panel Git section). - New `changes` icon registered (packages/ui). Verification: - typecheck clean; full app unit suite 1753 pass / 0 fail. - Visual: status-summary-panel snap (4 sections + rest->hover open/reveal) plus a dev:desktop walk in the real Electron host (dark theme, zh locale, live sidecar) - Git section populated from real VCS (+N/-N, branch main), Artifact section listing a real session file (sample-test.md, +76). - Codex adversarial review P1 findings fixed; 5 review threads resolved. Brought current with dev: one conflict in context/layout.tsx - dev had extracted the inline layout helpers into context/layout-state.ts / layout-projects.ts (#1056 slice work). Took dev's refactored layout.tsx and ported this branch's defaultSidePanelTab `| "files"` widening into layout-state.ts. Final state 0 behind dev; also merged #1135/#1136/#1137/#1139 cleanly (no file overlap). Deferred: dev:desktop worktree-indicator tooltip + non-git-hide are state-conditional and were not exercised live (hover open/reveal covered by the snap). session-side-panel.test.tsx isolation failure is the pre-existing mock.module warmup flake (its @/context/command mock omits matchKeybind/parseKeybind that terminal.tsx imports transitively); green in the full suite, tracked under the #1084 mock.module cleanup. Relates #1056.
What
The v2 SDK client's error interceptor only wrapped empty / unparseable
response bodies into a real
Error. Non-empty structured bodies — the commonopencode 4xx
NamedErrorshape, e.g.{ data: { message } }— passed straightthrough, so the
{ throwOnError: true }path threw a bare POJO.Every heavy SDK consumer in PawWork uses
throwOnError: true(ACPagent.ts/session.ts, the app session hooks, CLIrun.ts, github-copilot plugin), andthose surfaces format errors with
instanceof Error/.message. A thrown POJOtherefore surfaced as
[object Object]or a blank message instead of theserver's actual error text.
This extracts the interceptor into an exported
wrapClientErrorthat convertsnon-empty bodies into real
Errors on the throw path, preserving the originalparsed body and status under
.cause.Behavior
Errorwith message fromdata.message→message→name→ aMETHOD url -> statusdescription.Non-empty string bodies are wrapped too.
.cause = { body, status }.result.error): non-empty bodies returned unchanged,so existing field reads (
result.error.name,JSON.stringify, …) staybyte-for-byte identical.
same descriptive message as before (kept ASCII
->and exact format), now with.causeattached. This avoids regressingresult.errormessage quality, e.g.settings-worktreeserrorText()which would otherwise degrade to"{}".Errorinstances pass through untouched.Why this shape
Ports the idea from upstream
anomalyco/opencode11363170ca("fix(sdk): wrap thrown error bodies in Error"), adapted to PawWork's diverged
SDK:
packages/sdk/js/src/v2/client.ts) — thesurface app/run/acp actually use. The helper is inlined and exported from
that one file rather than upstream's separate
error-interceptor.tsmodule.throwOnError); PawWork's prior interceptor wrapped empty bodies on bothpaths and a
result.errorconsumer depends on the descriptive message.The v1
src/client.ts(plugin package) currently registers no errorinterceptor; wiring
wrapClientErrorthere is a clean, separable follow-up andis intentionally left out to keep this a single-file change.
Thanks to upstream (
@kitlangton) for the original fix.Test
packages/sdk/js/test/v2-client-error-interceptor.test.ts:{ data: { message } }body thrown viacreateOpencodeClient(...).global.health({ throwOnError: true })now rejectswith a real
Errorcarrying the extracted message (was the raw POJO).wrapClientError(message/name fallbacks,string bodies, result-tuple passthrough, empty + network descriptions,
Errorpassthrough).createOpencodeClient's real generated-clienterror pipeline (structured throw, raw result-tuple, empty body).
Verification
bun test(SDK package): 16 pass, 0 fail.bun run typecheck(SDK package): clean.lint:ciscope; no lint gate applies.