Skip to content

Commit 76f908b

Browse files
jlobue10claude
andcommitted
Skip downloads that were aborted while queued
A download aborted by invalidation or a cache directory change while it was still waiting in the concurrency limiter previously started anyway: downloadFile only listened for future abort events and never checked signal.aborted. The raced transfer held a download slot and, once the inactivity or deadline timeout hit, could settle the URL with a permanently persisted timeout error. Reject up front with the same retryable 'Request aborted' error the mid-flight abort path reports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Q2tBCuePZJeeCkDDxu1m3
1 parent 3bdeaaa commit 76f908b

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import os from 'node:os';
2+
import path from 'node:path';
3+
4+
const mockNetRequest = jest.fn();
5+
6+
jest.mock('electron', () => ({
7+
net: {
8+
request: mockNetRequest,
9+
},
10+
}));
11+
12+
const downloadFile = jest.requireActual<typeof import('./downloadFile')>('./downloadFile').default;
13+
14+
describe('downloadFile', () => {
15+
beforeEach(() => {
16+
mockNetRequest.mockReset();
17+
});
18+
19+
it('does not start a transfer whose signal was aborted while queued', async () => {
20+
const abortController = new AbortController();
21+
abortController.abort();
22+
23+
await expect(
24+
downloadFile('https://example.com/nft.png', path.join(os.tmpdir(), 'downloadFile-test-nft.png'), {
25+
signal: abortController.signal,
26+
}),
27+
).rejects.toThrow('Request aborted');
28+
29+
expect(mockNetRequest).not.toHaveBeenCalled();
30+
});
31+
});

packages/gui/src/electron/utils/downloadFile.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ export default async function downloadFile(
9090
throw new Error('Invalid URL');
9191
}
9292

93+
// A queued download can be aborted (invalidation, cache directory change)
94+
// before the concurrency limiter starts it. Without this check the transfer
95+
// would still run, hold a download slot, and could settle the URL with a
96+
// permanent timeout error. The error matches the mid-flight abort message so
97+
// the cache treats it as retryable.
98+
if (signal?.aborted) {
99+
throw new Error('Request aborted');
100+
}
101+
93102
const tempFilePath = `${localPath}.tmp`;
94103
const request = net.request(url);
95104
const outputStream = new WriteStreamPromise(tempFilePath, overrideFile);

0 commit comments

Comments
 (0)