fix(client): keep code and message from flat gateway error bodies

YA
1mo ago
+209-22

Why

WaveClient.parseErrorResponse reads only the canonical error envelope, {error:{code,message,details}}. Some gateway paths answer with a flat body instead:

  • spend-cap 402: {"error":"spend_cap_exceeded","code":"SPEND_CAP_TIER_BLOCKED","message":"Add a payment method to continue — this action exceeds your plan's included allotment.","dimension":"wave_clip_minutes"}
  • mesh 400: {"error":"missing x-wave-node"}

In the flat shape, body.error is a truthy string. The parser then read body.error.code and body.error.message, both undefined, and fell back to HTTP_402 / "Payment Required" and HTTP_400 / "Bad Request". The message that tells the user what to do was lost. This is what wave clip list, wave voice list-voices, wave transcribe get and wave mesh status showed in the @wave-av/cli@1.0.10 connectivity probe.

What changed

  • New src/client-errors.ts with parseErrorBody(body, status, statusText). It's a pure function, kept outside client.ts so that file doesn't grow past its size budget. It reads, in order:

    1. the canonical nested envelope
    2. top-level code and message
    3. a string error, used as the message, and as the code only when it is a slug (spend_cap_exceeded yes, missing x-wave-node no)

    Any other top-level fields, such as dimension or the original error slug, go to details. request_id in the body is still used when the response has no x-request-id header.

  • parseErrorResponse now calls it. Canonical-envelope behavior is unchanged, and empty or non-JSON bodies still fall back to the status.

  • src/__tests__/client-errors.test.ts covers the canonical, flat spend-cap, bare-string mesh, slug and empty/non-object cases. It also covers an end-to-end WaveClient.get against a stubbed 402, which now rejects with code: SPEND_CAP_TIER_BLOCKED, the real message, statusCode: 402 and the request ID.

  • CHANGELOG.md has a new [Unreleased] entry.

This does not touch the barrel exports or package.json. parseErrorBody is internal, just as client-types is.

Proof

  • npm run type-check: clean. npx vitest run: 25 files passed, 1 skipped; 232 tests passed, 3 skipped. npm run build succeeds.
  • Live, GET only, same API key through both builds. Control GET /v1/billing returned plan=free for both.
RoutePublished 2.1.3This branch
GET /v1/clips402 code=HTTP_402 "Payment Required"402 code=SPEND_CAP_TIER_BLOCKED "Add a payment method to continue — this action exceeds your plan's included allotment." (req f0411aa1-bb53-4b9a-aece-b28248e71c09)
GET /v1/mesh/topology400 code=HTTP_400 "Bad Request"400 code=HTTP_400 "missing x-wave-node" (req e4e851b0-703e-452d-8695-f661f1ff122f)
  • gitleaks: no leaks in the commit.

Follow-ups (not in this PR)

  • The gateway should still emit the canonical envelope from its spend-cap and mesh paths. This parser is the tolerant half of that fix.
  • The CLI's raw-route parser (wave-av/cli#86) already handles this shape. SDK-backed CLI commands get it once a release containing this change is published and the CLI's pinned @wave-av/sdk is bumped.

🤖 Generated with Claude Code


[!NOTE]
Medium Risk
Error parsing affects every failed HTTP response; Pulse changes alter return types and make getRevenueMetrics throw synchronously instead of 404ing over the network.

Overview
WaveClient now surfaces real gateway error codes and messages when responses use a flat JSON body (spend-cap 402 with code/message, mesh 400 with a string error) instead of only the nested {error:{code,message}} envelope. Parsing lives in new internal parseErrorBody (client-errors.ts), wired from parseErrorResponse, with unit and stubbed WaveClient.get coverage.

Pulse is aligned with what the gateway actually serves: getViewerAnalytics calls /v1/analytics/engagement and returns EngagementAnalytics (not the never-served /viewers shape). getRevenueMetrics throws WaveError with METHOD_NOT_SUPPORTED (501) instead of calling a route that always 404s. CHANGELOG.md documents the error fix.

Reviewed by Cursor Bugbot for commit 91d81f1d45a01aea8f8ca407e8031c87ea592102. Bugbot is set up for automated code reviews on this repo. Configure here.