From 1cf8f269f3c0a249c2aae10a55ccbbb8eca3023b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 27 Aug 2026 23:35:07 +0200 Subject: [PATCH] fix: emit final 100% download progress for empty response body onDownloadProgress never fired a completion event when the response body was empty (e.g. a 200 with Content-Length: 0), since the final callback in the transform stream's flush() was gated behind a chunk having been read. Report the final progress unconditionally, falling back to an empty chunk when the stream produced none. Closes #878 --- source/utils/body.ts | 4 +++- test/stream.ts | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/source/utils/body.ts b/source/utils/body.ts index c3ba19c..035b6d9 100644 --- a/source/utils/body.ts +++ b/source/utils/body.ts @@ -70,8 +70,10 @@ const withProgress = (stream: ReadableStream, totalBytes: number, on flush() { if (previousChunk) { transferredBytes += previousChunk.byteLength; - onProgress?.({percent: 1, totalBytes: Math.max(totalBytes, transferredBytes), transferredBytes}, previousChunk); } + + // Always report completion, even for an empty stream, so consumers can rely on a final 100% event. + onProgress?.({percent: 1, totalBytes: Math.max(totalBytes, transferredBytes), transferredBytes}, previousChunk ?? new Uint8Array()); }, })); }; diff --git a/test/stream.ts b/test/stream.ts index b9b5729..c9bc21c 100644 --- a/test/stream.ts +++ b/test/stream.ts @@ -555,6 +555,26 @@ test('onDownloadProgress cancels original response body', async t => { t.true(didReportProgress); }); +test('onDownloadProgress emits final 100% progress for empty response body', async t => { + const server = await createHttpTestServer(t); + server.get('/', (_request, response) => { + response.status(200).end(); + }); + + const progressEvents: Progress[] = []; + await ky(server.url, { + onDownloadProgress(progress) { + progressEvents.push(progress); + }, + }).text(); + + t.true(progressEvents.length > 0, 'Should emit at least one progress event'); + const finalProgress = progressEvents.at(-1); + t.truthy(finalProgress); + t.is(finalProgress!.percent, 1); + t.is(finalProgress!.transferredBytes, finalProgress!.totalBytes); +}); + test('forced retry custom request keeps upload progress', async t => { const server = await createHttpTestServer(t, {bodyParser: false}); let requestCount = 0;