Make the download deadline an idle deadline and cancel failed bodies (closes #24)
check / check (push) Successful in 14s
check / check (push) Successful in 14s
downloadTimeoutMs now aborts a file or thumbnail download only when no bytes have arrived for that long (default 60 s, was a 600 s cap on the whole transfer), so a slow download that keeps making progress completes. The abort reason is still a TimeoutError, so retry classification is unchanged. A download that fails before reading the whole response body cancels it, including when the temp file cannot be opened or the header is malformed, so a failed file no longer holds its connection. Model: opus-5-5
This commit is contained in:
@@ -1337,6 +1337,102 @@ describe("download retries: corruption is not retried", () => {
|
||||
|
||||
expect(requests()).toBe(1);
|
||||
});
|
||||
|
||||
it("cancels the response body when decryption fails", async () => {
|
||||
// A backup run carries on past a failed file, so a body left open on
|
||||
// failure would hold its connection until garbage collection, once
|
||||
// per failed file. This body delivers a corrupt chunk and then stays
|
||||
// open, so only a cancel from the downloader can close it.
|
||||
const corrupted = Uint8Array.from(multiChunk.body);
|
||||
corrupted[10] ^= 0xff;
|
||||
let cancelled = false;
|
||||
const fetch = (async () =>
|
||||
new Response(
|
||||
new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(corrupted);
|
||||
},
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
)) as typeof globalThis.fetch;
|
||||
const api = new ApiClient({ fetch, retry: { ...noWait, attempts: 1 } });
|
||||
const file = buildMockEnteFile(
|
||||
multiChunkKey,
|
||||
multiChunk.header,
|
||||
multiChunk.header,
|
||||
);
|
||||
const outPath = join(mkdtempSync(join(testDir, "cancel-")), "c.bin");
|
||||
|
||||
await expect(downloadFile(api, file, outPath)).rejects.toThrow(
|
||||
/authentication failed/i,
|
||||
);
|
||||
|
||||
expect(cancelled).toBe(true);
|
||||
});
|
||||
|
||||
it("cancels the response body when the temp file cannot be opened", async () => {
|
||||
// The download fails before a byte of the body is read, so the body
|
||||
// is still open and only a cancel from the downloader can close it.
|
||||
let cancelled = false;
|
||||
const fetch = (async () =>
|
||||
new Response(
|
||||
new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(multiChunk.body);
|
||||
},
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
)) as typeof globalThis.fetch;
|
||||
const api = new ApiClient({ fetch, retry: { ...noWait, attempts: 1 } });
|
||||
const file = buildMockEnteFile(
|
||||
multiChunkKey,
|
||||
multiChunk.header,
|
||||
multiChunk.header,
|
||||
);
|
||||
const outPath = join(
|
||||
mkdtempSync(join(testDir, "cancel-")),
|
||||
"missing",
|
||||
"c.bin",
|
||||
);
|
||||
|
||||
await expect(downloadFile(api, file, outPath)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
|
||||
expect(cancelled).toBe(true);
|
||||
});
|
||||
|
||||
it("cancels the response body when the header is malformed", async () => {
|
||||
// The header is rejected before the body is read, so the body is
|
||||
// still open and only a cancel from the downloader can close it.
|
||||
let cancelled = false;
|
||||
const fetch = (async () =>
|
||||
new Response(
|
||||
new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(multiChunk.body);
|
||||
},
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
)) as typeof globalThis.fetch;
|
||||
const api = new ApiClient({ fetch, retry: { ...noWait, attempts: 1 } });
|
||||
const shortHeader = multiChunk.header.subarray(0, 5);
|
||||
const file = buildMockEnteFile(multiChunkKey, shortHeader, shortHeader);
|
||||
const outPath = join(mkdtempSync(join(testDir, "cancel-")), "c.bin");
|
||||
|
||||
await expect(downloadFile(api, file, outPath)).rejects.toThrow();
|
||||
|
||||
expect(cancelled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user