Repository navigation
Keep the judge response body readable under Bun - #35
Merged
Merged
Conversation
A judge request made from a Bun process failed with "TypeSafe returned an unreadable or unexpected response.": @typesafe-ai/sdk 0.7.0 buffers a response by taking a reader on its body and releasing it, and Bun counts that as using the body, so the SDK's own `response.text()` threw `TypeError: Body already used`. A response this package builds itself is stamped by Bun the same way, so the fault is in the transport, not in one backend or one caller. Under Bun, the transport handed to the client now reads the body and hands over a response with none, answering `text()` and `json()` from the bytes already read; status, statusText and headers are carried across, without the length and encoding that described bytes no longer on the wire. The wrapper sits ahead of both the backend's path rewrite and the caller's own transport, so a caller that wraps fetch stays harmless next to it. Under Node the transport is exactly what it was: the caller's fetch, or none at all. Verified against a local HTTP server with @typesafe-ai/sdk 0.7.0: the parsed answers returned and one request per judgment, a 429 with its Retry-After header reported unchanged, an aborted request rejected as aborted, and a caller-supplied fetch still called once per request. The same tests pass under Bun 1.4.4 and Node 26 here, and the file is part of the Node test suite.
The body-buffering fix can only regress on a runtime that runs it, so the workflow gains a Bun 1.4.4 job beside the Node matrix: it installs the lockfile, builds, typechecks, and runs tests/bun-transport.test.ts under Bun, and CI passed requires it. Five of that file's six tests fail under Bun without the fix. The README says Bun is supported and why the transport differs there, the CI doc lists the new job, and the CHANGELOG records the fix, the SDK and Bun versions measured, and how it was verified.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Under Bun — the runtime Pi's compiled binary embeds — every judge request fails with
TypeSafe returned an unreadable or unexpected response.The cause is masked:@typesafe-ai/sdkbuffers a response by taking a reader onresponse.clone().bodyand another onresponse.body, releases both locks, and then readsresponse.text(). Releasing a lock without reading leaves a Node body usable; Bun marks it used, so the SDK's owntext()throwsTypeError: Body already used.This change reads the body once inside the transport this package hands the SDK, and hands back a response with no body whose
text()andjson()answer from the bytes already read. The SDK's buffering step then finds no reader to take, so the body it later reads is the one we hold. Status, status text and headers survive;content-lengthandcontent-encodingare dropped because the body is re-emitted. The wrapper sits ahead of both the backend path rewrite and any caller-suppliedfetch, so an integration that already works around this on its own keeps working — two replays stacked are harmless.Node is untouched: with no
fetchoption, no transport is passed at all; with one, the caller's own goes through unchanged.Cost: under Bun a response is buffered before the SDK sees it and no longer streams. The SDK buffers every response anyway — that is the mechanism that broke here.
Verification
npm ciandnpm run checkpass.tests/bun-transport.test.tscovers, against a real local HTTP server: oneevaluate()through the public entry point, a non-2xx status with its headers reported unchanged, an abort during body delivery rejecting as aborted, the transport each runtime is handed, a caller-suppliedfetchcalled once per request, and a backend that rewrites its model list.Measured on this machine (Node 26.12.1, Bun 1.4.4): before, the Bun call fails with the
responseerror while Node returns the parsed answers; after, both return them with exactly one HTTP request per call. Wall time over 20 calls is inside run-to-run noise on both runtimes, before and after. Without the fix, five of the six new tests fail under Bun.A new
Check (Bun 1.4.4)job builds, typechecks and runs that test file under Bun, and joins theCI passedgate. Every one of its shell steps was run here; its two setup actions cannot run outside GitHub.Live test: run once, under Bun 1.4.4, against the real service with the synthetic message from
scripts/live-smoke.mjs. It passed: one request,jev-1.14.0, 408 input and 70 output tokens, 230 ms, and the three answers its assertions require. No key, request state or response body is reproduced here. The failing path itself was reproduced against a loopback server, where the same code reported theresponseerror before this change.Compatibility and release notes
No public API change, no new dependency, no version bump. Behavior differs only under Bun; Node callers see no change at all. The one thing a Bun caller could notice is that a response is buffered rather than streamed — which the SDK already did.