Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

## Unreleased

- A file download refused because the computer's response carried no usable byte length now releases
the connection before reporting the refusal. The unread body could be as large as the whole
download budget, so a computer reached through a proxy that re-chunks left a transfer running and a
connection checked out of the pool on every attempt.
### A Bot's turn in a group is no longer offered coordination tools it cannot call

A Bot answering another Bot in a group conversation was offered `ask_person`, and `message_bot`
Expand Down
4 changes: 4 additions & 0 deletions server/src/computer/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ export function createComputerTransport(
const bytes =
length !== null && /^\d+$/.test(length) ? Number(length) : Number.NaN;
if (!response.body || !Number.isSafeInteger(bytes) || bytes < 0) {
// Nothing else will ever read this body, so release it here rather than
// leaving the whole download budget checked out of the pool. A cancel that
// fails must not replace the refusal the caller is told about.
await response.body?.cancel().catch(() => undefined);
throw new ComputerUnavailableError(
"The assistant's computer returned an invalid file download.",
);
Expand Down
34 changes: 34 additions & 0 deletions server/tests/computer-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,40 @@ describe("computer client", () => {
);
});

test("releases the connection it refuses to hand back", async () => {
let cancelled = false;
const body = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(Uint8Array.from([1, 2, 3]));
},
cancel() {
cancelled = true;
},
});
const client = clientWith(() => new Response(body));

await expect(client.download("x")).rejects.toThrow(
ComputerUnavailableError,
);
expect(cancelled).toBe(true);
});

test("still names the invalid download when releasing it fails", async () => {
const body = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(Uint8Array.from([1, 2, 3]));
},
cancel() {
throw new Error("socket already gone");
},
});
const client = clientWith(() => new Response(body));

await expect(client.download("x")).rejects.toThrow(
ComputerUnavailableError,
);
});

test.each([
[404, WorkspaceNotFoundError],
[413, WorkspaceTooLargeError],
Expand Down