fix(server): release a refused download's body before reporting it - #732
Open
aniruddhaadak80 wants to merge 1 commit into
Open
aniruddhaadak80 wants to merge 1 commit into
aniruddhaadak80 wants to merge 1 commit into
Conversation
A download was refused when the computer's response carried no usable content-length, but the body was left unread: the throw happened with the stream still checked out. Nothing else ever reads it, and it can be as large as the whole workspace download budget. A computer reached through a proxy that re-chunks answers 200 with Transfer-Encoding and no content-length, so every attempt left a transfer running and a connection held until it was collected. A Bot retrying a download turned one refusal into a slow leak rather than one clear error. The refusal branch above already consumed its body via response.json(); this brings the remaining refusal in line, and matches the release idiom already used in provider-oauth.ts and voice/. A cancel that fails must not replace the refusal the caller is told about, so it is guarded and the ComputerUnavailableError still surfaces.
aniruddhaadak80
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso,
mxmzb and
tylerslaton
as code owners
October 4, 2026 08:58
This branch has not been deployed
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 this changes
A file download that OpenBot refuses because the computer's response carried no usable
content-lengthnow releases the response body before it throws.download()inserver/src/computer/client.tsreadscontent-lengthto learn how large the download is, and refuses when the header is missing or not a plain integer:At that point the body is unread, and nothing in the codebase ever reads it — the function throws instead of returning it. So the stream is abandoned while still checked out, holding a transfer and a pooled connection until it is eventually collected. The body can be as large as the whole workspace download budget.
This is reachable in an ordinary deployment. A computer reached through a proxy that re-chunks, or any remote provider behind
COMPUTER_AGENT_URL, answers200withTransfer-Encoding: chunkedand nocontent-length. Every attempt then leaves a transfer running. A Bot that retries a download turns a single clear refusal into a slow leak instead.The refusal immediately above it already gets this right — the
!response.okbranch consumes its body throughresponse.json()before callingthrowMappedError. This change brings the one remaining refusal in line, and uses the release idiom the server already uses inprovider-oauth.tsandvoice/summary.ts.The cancel is guarded, because a cancel that fails must not replace the refusal the caller is told about. There is a regression test for exactly that.
Where it runs
Boundary and audit
Every acting call still goes through the gateway: resolve, decide, audit, then act.
This is not an acting call. It is the refusal path for a download whose metadata could not be trusted, and it stays on that path — the same
ComputerUnavailableErroris thrown, with the same message, for the same condition.New refusals and new failures each write a row.
No new refusal is introduced. The condition that refuses is unchanged; only what happens to the socket before the refusal is different.
Nothing new is trusted from the client that the server can resolve itself.
No new input is read. The only value used is the computer's own
content-length, and it is still only used to compute a byte count — a length that cannot be trusted is refused, exactly as before.Changelog
A line in
CHANGELOG.mdunderUnreleased.Added. It describes the deployment-visible difference: a refused download no longer holds a transfer open.
Proof
What I ran, from the repository root:
bun test server/tests/computer-client.test.ts— 27 pass, 0 fail. The two new tests are in the existingcomputer file downloadsblock:releases the connection it refuses to hand backbuilds aReadableStreamwith acancelspy and nocontent-length, asserts the refusal as before, and asserts the body was released. This test fails onmain— it reportscancelledasfalse, because the body is abandoned at the throw. I confirmed the failure before applying the fix.still names the invalid download when releasing it failsmakescancel()reject and asserts the caller still getsComputerUnavailableErrorrather than the cancel's own error.bun run --filter server typecheck— exit code 0.bunx biome lint --error-on-warningsandbunx biome formaton the two changed files — clean, no fixes applied.I have not run this against a live
agent-computerbehind a re-chunking proxy; the regression test drives the transport'sfetchImplseam directly, which is where the abandoned body was observable.