From eae9121da858f4af9002df125286f83e098eb9e5 Mon Sep 17 00:00:00 2001 From: Matias Simon Date: Thu, 24 Sep 2026 14:57:02 +0200 Subject: [PATCH] Don't take the queue down when cancelling a transforming file `transformFile` is asynchronous, and a file is already UPLOADING while it runs. For that window it has no `xhr`, which broke `cancelUpload` twice over. `_getFilesWithXhr(file.xhr)` was called with `undefined` and matched every file whose `xhr` was also undefined -- that is, everything still queued. Cancelling one upload set them all to CANCELED and emitted a `canceled` event for each. Group by the request only when there is one; without it the file is its own group. Then, because nothing rechecked the status, the transform callback went on to upload the file the user had just cancelled. Bail out instead when every file in the batch has been cancelled. Refs #2231 --- .changeset/cancel-during-transform.md | 5 ++ packages/dropzone/src/dropzone.ts | 15 +++++- packages/dropzone/test/unit-tests/all.js | 59 ++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 .changeset/cancel-during-transform.md diff --git a/.changeset/cancel-during-transform.md b/.changeset/cancel-during-transform.md new file mode 100644 index 000000000..0b8a896b2 --- /dev/null +++ b/.changeset/cancel-during-transform.md @@ -0,0 +1,5 @@ +--- +"dropzone": patch +--- + +Fix cancelling an upload while `transformFile` is still running. The file had no `xhr` yet, so it was grouped with every other file that had not started and the whole queue was cancelled with it — and the file itself was still sent once the transform finished. diff --git a/packages/dropzone/src/dropzone.ts b/packages/dropzone/src/dropzone.ts index 89131f7ec..c833c14d4 100644 --- a/packages/dropzone/src/dropzone.ts +++ b/packages/dropzone/src/dropzone.ts @@ -1345,7 +1345,12 @@ export default class Dropzone extends Emitter { // set to CANCELED. cancelUpload(file: DropzoneFile) { if (file.status === Dropzone.UPLOADING) { - let groupedFiles = this._getFilesWithXhr(file.xhr!); + // A file cancelled while `transformFile` is still running is already + // UPLOADING but has no request yet. Grouping by an undefined `xhr` then + // matched every other file that had not started either -- the whole + // queue -- and cancelled all of them. Without a request there is nobody + // to share it with, so the group is just this file. See #2231. + let groupedFiles = file.xhr ? this._getFilesWithXhr(file.xhr) : [file]; for (let groupedFile of groupedFiles) { groupedFile.status = Dropzone.CANCELED; } @@ -1395,6 +1400,14 @@ export default class Dropzone extends Emitter { uploadFiles(files: DropzoneFile[]) { this._transformFiles(files, (transformedFiles: (DropzoneFile | Blob)[]) => { + // `transformFile` is asynchronous and can take a while -- resizing or + // compressing a large image, or asking a server to presign the upload -- + // and the user can cancel or remove the file in the meantime. Sending it + // now would upload data the UI already reported as canceled. See #2231. + if (files.every((file) => file.status === Dropzone.CANCELED)) { + return; + } + // Options are often read straight out of markup or a config file, so // `chunkSize` can arrive as a string. `start + "2097152"` would then // concatenate instead of adding, and every chunk after the first would diff --git a/packages/dropzone/test/unit-tests/all.js b/packages/dropzone/test/unit-tests/all.js index 7d00c6452..4e4f4afea 100644 --- a/packages/dropzone/test/unit-tests/all.js +++ b/packages/dropzone/test/unit-tests/all.js @@ -692,6 +692,65 @@ describe("Dropzone", function () { }, 10); })); + it("should not cancel other queued files if the upload has not started yet", () => + new Promise((done) => { + dropzone.accept = (file, done) => done(); + // A transform that never finishes: the file is UPLOADING but has no + // xhr, which is what used to group it with every queued file. + dropzone.options.transformFile = () => {}; + dropzone.options.parallelUploads = 1; + + let uploading = getMockFile("text/html", "uploading"); + let queued1 = getMockFile("text/html", "queued1"); + let queued2 = getMockFile("text/html", "queued2"); + + dropzone.addFile(uploading); + dropzone.addFile(queued1); + dropzone.addFile(queued2); + + return setTimeout(function () { + expect(uploading.status).toBe(Dropzone.UPLOADING); + expect(uploading.xhr).toBe(undefined); + + let canceled = []; + dropzone.on("canceled", (file) => canceled.push(file.upload.filename)); + + dropzone.cancelUpload(uploading); + + expect(uploading.status).toBe(Dropzone.CANCELED); + // Only the file the user cancelled. The queue then moves on, so + // the next one starts and the last one waits its turn. + expect(canceled).toEqual(["uploading"]); + expect([queued1.status, queued2.status]).toEqual([Dropzone.UPLOADING, Dropzone.QUEUED]); + return done(); + }, 10); + })); + + it("should not send a file that was canceled while it was being transformed", () => + new Promise((done) => { + dropzone.accept = (file, done) => done(); + + let finishTransform = null; + dropzone.options.transformFile = (file, transformDone) => + (finishTransform = () => transformDone(file)); + + let mockFile = getMockFile(); + dropzone.addFile(mockFile); + + return setTimeout(function () { + expect(mockFile.status).toBe(Dropzone.UPLOADING); + + dropzone.cancelUpload(mockFile); + expect(mockFile.status).toBe(Dropzone.CANCELED); + + // The transform only finishes now, after the user gave up on it. + finishTransform(); + + expect(mockFile.xhr).toBe(undefined); + return done(); + }, 10); + })); + it("should properly cancel all files with the same XHR if uploadMultiple is true", () => new Promise((done) => { let mock1 = getMockFile();