fix(client): keep code and message from flat gateway error bodies
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.tswithparseErrorBody(body, status, statusText). It's a pure function, kept outsideclient.tsso that file doesn't grow past its size budget. It reads, in order:- the canonical nested envelope
- top-level
codeandmessage - a string
error, used as the message, and as the code only when it is a slug (spend_cap_exceededyes,missing x-wave-nodeno)
Any other top-level fields, such as
dimensionor the originalerrorslug, go todetails.request_idin the body is still used when the response has nox-request-idheader. -
parseErrorResponsenow calls it. Canonical-envelope behavior is unchanged, and empty or non-JSON bodies still fall back to the status. -
src/__tests__/client-errors.test.tscovers the canonical, flat spend-cap, bare-string mesh, slug and empty/non-object cases. It also covers an end-to-endWaveClient.getagainst a stubbed 402, which now rejects withcode: SPEND_CAP_TIER_BLOCKED, the real message,statusCode: 402and the request ID. -
CHANGELOG.mdhas 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 buildsucceeds.- Live, GET only, same API key through both builds. Control
GET /v1/billingreturnedplan=freefor both.
| Route | Published 2.1.3 | This branch |
|---|---|---|
GET /v1/clips | 402 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/topology | 400 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/sdkis 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.