Skip to content

fix: read error response bodies once in the TypeScript client#39

Merged
santiagomed merged 1 commit into
mainfrom
santiagomed/fix-error-body-read
Jul 25, 2026
Merged

fix: read error response bodies once in the TypeScript client#39
santiagomed merged 1 commit into
mainfrom
santiagomed/fix-error-body-read

Conversation

@santiagomed

Copy link
Copy Markdown
Contributor

Problem

A fetch Response body is a one-shot stream. The error paths in the generated TypeScript client called response.json() and, when that failed, fell back to response.text() on the same response. Any non-JSON error body (for example a plain-text 404 from the gateway) therefore crashed with:

TypeError: Body is unusable: Body has already been read

In Client.request that TypeError escaped to the outer catch and was rethrown as ApiError with status 0 / NETWORK_ERROR, completely masking the real HTTP status and body. In practice this means every non-JSON API error a consumer hits is unreadable.

The same pattern (response.json().catch(() => response.text())) existed twice in the OAuth2 token flows.

The Python client is not affected: requests buffers the body, so .text remains readable after a failed .json().

Fix

  • xdk-gen/templates/typescript/main_client.j2: read the error body once as text, attempt JSON.parse, and fall back to the raw text. The ApiError now always carries the real status, status text, and body. For JSON error bodies the behavior is unchanged.
  • xdk-gen/templates/typescript/oauth2_auth.j2: read the error body once as text in both token-exchange error paths.

Tests

New generated test file tests/client_errors.test.ts (template test_client_errors.j2, registered in generator.rs) covering Client.request and both OAuth2 flows with JSON, plain-text, and empty error bodies. The tests use real Response objects rather than mocks with independent json()/text() resolvers — the existing generic tests mock those methods separately, which is exactly why this bug was never caught. Verified the non-JSON-body test fails against the previous implementation and passes with the fix.

Validation: cargo test green, SDK regenerated with cargo run -- typescript --latest true, npm run build green, full jest suite 1153/1153 passing.

A fetch Response body is a one-shot stream. The error paths in the
generated client and OAuth2 helper called response.json() and fell back
to response.text() on the same response, so any non-JSON error body
(e.g. a plain-text 404 from the gateway) crashed with "Body is
unusable: Body has already been read" instead of surfacing the real
status and body.

Read the body once as text and attempt JSON.parse, and add generated
regression tests that use real Response objects so one-shot body
semantics are actually exercised.
@santiagomed
santiagomed merged commit 8905614 into main Jul 25, 2026
4 checks passed
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