From 536d94afd9da8f55b4e4976bd527909946e75e4a Mon Sep 17 00:00:00 2001 From: Matias Simon Date: Sun, 13 Sep 2026 12:45:38 +0200 Subject: [PATCH] Abort every in-flight chunk when a chunked upload is canceled `cancelUpload` aborted `file.xhr`, which only ever holds the request that started last. With `parallelChunkUploads` the other chunks kept streaming to the server for a file the UI already showed as canceled, and if the last chunk had finished, `file.xhr` pointed at a completed request and the abort did nothing at all. Each chunk already carries its own `xhr`, so walk `file.upload.chunks` and abort the ones still uploading. Non-chunked uploads keep aborting `file.xhr` as before. Fixes #2366 Co-Authored-By: Claude Opus 5 --- .changeset/cancel-parallel-chunks.md | 9 +++++++ packages/dropzone/src/dropzone.ts | 13 +++++++++- packages/dropzone/test/unit-tests/all.js | 30 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 .changeset/cancel-parallel-chunks.md diff --git a/.changeset/cancel-parallel-chunks.md b/.changeset/cancel-parallel-chunks.md new file mode 100644 index 000000000..f93589624 --- /dev/null +++ b/.changeset/cancel-parallel-chunks.md @@ -0,0 +1,9 @@ +--- +"dropzone": patch +--- + +Fix `cancelUpload` leaving parallel chunks uploading. + +`file.xhr` only holds the request that started last, so cancelling a chunked upload running with `parallelChunkUploads` aborted that one request and left every other in-flight chunk streaming to the server — burning the user's bandwidth and writing orphaned chunks for a file the UI already showed as canceled. + +Every chunk keeps its own request, so `cancelUpload` now aborts all of the ones still running. Uploads that are not chunked are unaffected. diff --git a/packages/dropzone/src/dropzone.ts b/packages/dropzone/src/dropzone.ts index a4e584576..768557fe7 100644 --- a/packages/dropzone/src/dropzone.ts +++ b/packages/dropzone/src/dropzone.ts @@ -1321,7 +1321,18 @@ export default class Dropzone extends Emitter { for (let groupedFile of groupedFiles) { groupedFile.status = Dropzone.CANCELED; } - if (typeof file.xhr !== "undefined") { + if (file.upload.chunked && file.upload.chunks) { + // `file.xhr` only ever holds the request that started last, so with + // `parallelChunkUploads` aborting it leaves every other chunk + // streaming to the server for a file the user has already canceled. + // Each chunk keeps its own request, so abort them all. See #2366. + for (let chunk of file.upload.chunks) { + if (chunk && chunk.xhr && chunk.status === Dropzone.UPLOADING) { + chunk.status = Dropzone.CANCELED; + chunk.xhr.abort(); + } + } + } else if (typeof file.xhr !== "undefined") { file.xhr.abort(); } for (let groupedFile of groupedFiles) { diff --git a/packages/dropzone/test/unit-tests/all.js b/packages/dropzone/test/unit-tests/all.js index 3a71785c8..f6f21b4d9 100644 --- a/packages/dropzone/test/unit-tests/all.js +++ b/packages/dropzone/test/unit-tests/all.js @@ -2162,6 +2162,36 @@ describe("Dropzone", function () { }, 10); })); + it("should abort every chunk still in flight when the upload is canceled", () => + new Promise((done) => { + dropzone.options.chunking = true; + dropzone.options.chunkSize = 1; + dropzone.options.parallelChunkUploads = 3; + + let file = getMockFile("text/html", "chunked-file", ["abcdef"]); + dropzone.addFile(file); + + setTimeout(function () { + // Three of the six chunks are in flight, each with its own + // request. + expect(requests.length).toBe(3); + expect(requests.map((request) => request.aborted)).toEqual([false, false, false]); + + dropzone.cancelUpload(file); + + expect(file.status).toBe(Dropzone.CANCELED); + // `file.xhr` only holds the request that started last, so the + // other two used to keep streaming to the server. See #2366. + expect(requests.map((request) => request.aborted)).toEqual([true, true, true]); + expect(file.upload.chunks.map((chunk) => chunk.status)).toEqual([ + Dropzone.CANCELED, + Dropzone.CANCELED, + Dropzone.CANCELED, + ]); + done(); + }, 10); + })); + it("should never start fewer than one chunk", () => new Promise((done) => { startChunked({ parallelChunkUploads: 0 });