Skip to content

Commit c4d1f37

Browse files
authored
fix(memory): abort batch upload response reads (#95111)
* fix(memory): abort batch upload response reads * test(memory): stabilize batch upload abort proof
1 parent ba43be9 commit c4d1f37

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

packages/memory-host-sdk/src/host/batch-upload.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@ function streamingTextResponse(params: {
3131
return new Response(stream, { status: params.status, headers: params.headers });
3232
}
3333

34+
function stallingResponse(params: { status: number; onCancel: () => void }): Response {
35+
const reader = {
36+
read: () => new Promise<ReadableStreamReadResult<Uint8Array>>(() => {}),
37+
cancel: async () => {
38+
params.onCancel();
39+
},
40+
releaseLock: () => undefined,
41+
} as ReadableStreamDefaultReader<Uint8Array>;
42+
43+
return {
44+
status: params.status,
45+
ok: params.status >= 200 && params.status < 300,
46+
headers: new Headers(),
47+
body: { getReader: () => reader },
48+
} as Response;
49+
}
50+
3451
describe("uploadBatchJsonlFile", () => {
3552
beforeEach(() => {
3653
vi.clearAllMocks();
@@ -108,4 +125,70 @@ describe("uploadBatchJsonlFile", () => {
108125
).rejects.toThrow("file upload failed: response body too large: 64 bytes (limit: 8 bytes)");
109126
expect(canceled).toBe(true);
110127
});
128+
129+
it("passes caller abort signals through non-ok file-upload response snippets", async () => {
130+
let canceled = false;
131+
remoteHttpMock.mockImplementationOnce(async (params) => {
132+
return await params.onResponse(
133+
stallingResponse({
134+
status: 500,
135+
onCancel: () => {
136+
canceled = true;
137+
},
138+
}),
139+
);
140+
});
141+
const controller = new AbortController();
142+
const upload = uploadBatchJsonlFile({
143+
client: {
144+
baseUrl: "https://memory.example/v1",
145+
headers: { Authorization: "Bearer test" },
146+
},
147+
requests: [{ input: "one" }],
148+
errorPrefix: "file upload failed",
149+
signal: controller.signal,
150+
});
151+
152+
await new Promise((resolve) => {
153+
setTimeout(resolve, 0);
154+
});
155+
controller.abort(new Error("upload aborted"));
156+
157+
await expect(upload).rejects.toThrow("upload aborted");
158+
expect(canceled).toBe(true);
159+
expect(remoteHttpMock.mock.calls[0]?.[0].signal).toBe(controller.signal);
160+
});
161+
162+
it("passes caller abort signals through successful file-upload JSON reads", async () => {
163+
let canceled = false;
164+
remoteHttpMock.mockImplementationOnce(async (params) => {
165+
return await params.onResponse(
166+
stallingResponse({
167+
status: 200,
168+
onCancel: () => {
169+
canceled = true;
170+
},
171+
}),
172+
);
173+
});
174+
const controller = new AbortController();
175+
const upload = uploadBatchJsonlFile({
176+
client: {
177+
baseUrl: "https://memory.example/v1",
178+
headers: { Authorization: "Bearer test" },
179+
},
180+
requests: [{ input: "one" }],
181+
errorPrefix: "file upload failed",
182+
signal: controller.signal,
183+
});
184+
185+
await new Promise((resolve) => {
186+
setTimeout(resolve, 0);
187+
});
188+
controller.abort(new Error("upload json aborted"));
189+
190+
await expect(upload).rejects.toThrow("upload json aborted");
191+
expect(canceled).toBe(true);
192+
expect(remoteHttpMock.mock.calls[0]?.[0].signal).toBe(controller.signal);
193+
});
111194
});

packages/memory-host-sdk/src/host/batch-upload.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export async function uploadBatchJsonlFile(params: {
1616
requests: unknown[];
1717
errorPrefix: string;
1818
maxResponseBytes?: number;
19+
signal?: AbortSignal;
1920
}): Promise<string> {
2021
const baseUrl = normalizeBatchBaseUrl(params.client);
2122
const jsonl = params.requests.map((request) => JSON.stringify(request)).join("\n");
@@ -31,19 +32,21 @@ export async function uploadBatchJsonlFile(params: {
3132
url: `${baseUrl}/files`,
3233
ssrfPolicy: params.client.ssrfPolicy,
3334
fetchImpl: params.client.fetchImpl,
35+
signal: params.signal,
3436
init: {
3537
method: "POST",
3638
headers: buildBatchHeaders(params.client, { json: false }),
3739
body: form,
3840
},
3941
onResponse: async (fileRes) => {
4042
if (!fileRes.ok) {
41-
const text = await readResponseTextSnippet(fileRes);
43+
const text = await readResponseTextSnippet(fileRes, { signal: params.signal });
4244
throw new Error(`${params.errorPrefix}: ${fileRes.status} ${text}`);
4345
}
4446
return (await readResponseJsonWithLimit(fileRes, {
4547
errorPrefix: params.errorPrefix,
4648
maxBytes: params.maxResponseBytes,
49+
signal: params.signal,
4750
})) as { id?: string };
4851
},
4952
});

0 commit comments

Comments
 (0)