Skip to content

Release the thumbnail queue when a file cannot be read - #2370

Open
enyo wants to merge 1 commit into
mainfrom
thumbnail-read-error
Open

Release the thumbnail queue when a file cannot be read#2370
enyo wants to merge 1 commit into
mainfrom
thumbnail-read-error

Conversation

@enyo

@enyo enyo commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Fixes #2365.

The bug

createThumbnail only ever attached fileReader.onload. A FileReader that fails — the file was moved or deleted after being dropped, the OS refused the read, another process holds a lock, the drive went away — fires error instead, so load never runs and the callback is never invoked.

That callback is what releases _processingThumbnail:

this._processingThumbnail = true;
let file = this._thumbnailQueue.shift();
return this.createThumbnail(file, , (dataUrl) => {
  
  this._processingThumbnail = false;   // never reached
  return this._processThumbnailQueue();
});

So the lock is held for the lifetime of the page. Every image added afterwards sits in _thumbnailQueue and never gets a thumbnail. And because transformFile routes through createThumbnail as soon as resizeWidth or resizeHeight is set, the affected upload never calls done() either and simply hangs.

#2218 added img.onerror in createThumbnailFromUrl, which covers an image that cannot be decoded. This is the stage before it: a file that cannot be read.

The fix

Forward the error event to the callback, which is what the img.onerror path already did — _processThumbnailQueue tests typeof dataUrl === "string" and emits dictThumbnailError for anything else. One failed file now reports an error and the queue carries on; resizeImage falls back to the untransformed file the same way it does for an undecodable image, so the upload still goes out.

The signature was wrong, so it now says what it does

DropzoneThumbnailCallback declared dataUrl: string, but neither failure path has ever passed a string — that is what the typeof check in _processThumbnailQueue exists for. The declaration was only holding because of two casts (callback(e as unknown as string) and img.onerror = callback as unknown as OnErrorEventHandler). The type is now string | Event and both casts are gone.

Making it honest turned up a third call site the casts had been hiding: displayExistingFile passed (thumbnail: string) => this.emit("thumbnail", …) straight through, so an image URL that failed to load emitted the error event as the thumbnail and the preview ended up with img.src set to "[object Event]" — the same defect #2218 fixed for the queue path. It now leaves the preview alone and still runs the callback.

Tests

  • should emit an error and keep the queue moving if a file can't be read — makes readAsDataURL dispatch error, asserts the file is reported with dictThumbnailError and _processingThumbnail is back to false, then adds a real PNG and waits for its thumbnail event. Times out against main.
  • should not emit a thumbnail when the image url fails to load — against main: expected Event{ isTrusted: true } to be null.

@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 79.67% 886 / 1112
🔵 Statements 80.11% 935 / 1167
🔵 Functions 93.42% 199 / 213
🔵 Branches 76.6% 514 / 671
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/dropzone/src/dropzone.ts 77.82% 75.04% 93.33% 77.23% 238, 248, 273, 279-282, 339, 366, 409-410, 469, 577, 599, 618-619, 726, 749-752, 789-791, 821-824, 847, 1021, 1051, 1055, 1144, 1178, 1188-1190, 1207-1208, 1214-1247, 1360, 1418, 1467-1468, 1596, 1620-1621, 1675, 1709, 1729-1760, 1768, 1782-1786, 1797, 1810-1815, 1828-1829, 1833-1834, 1852-1853, 1931-1946, 1991, 2007, 2178-2180, 2215, 2225, 2258-2292, 2299-2400, 2409-2412
Generated in workflow #142 for commit 0e3625d by the Vitest Coverage Report Action

`createThumbnail` only handled `FileReader`'s `load` event. A file that
had been moved, locked by another process, or was otherwise unreadable
since it was dropped fires `error` instead, so the callback was never
invoked and `_processThumbnailQueue` kept `_processingThumbnail` set
forever. No file added afterwards ever got a thumbnail, and where
`transformFile` goes through `createThumbnail` -- which it does as soon
as `resizeWidth` or `resizeHeight` is set -- the upload never started
either.

Hand the error event to the callback, which is what the `img.onerror`
path in `createThumbnailFromUrl` already did, so the file is reported
with `dictThumbnailError` and the queue carries on.

`DropzoneThumbnailCallback` now declares that: `string | Event` rather
than `string`, which is what both failure paths have always passed. The
two casts that hid it are gone, and with the real signature in place the
compiler found `displayExistingFile` emitting the error event as a
thumbnail, so a preview whose image URL failed to load ended up with
`img.src` set to "[object Event]".

Fixes #2365

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@enyo
enyo force-pushed the thumbnail-read-error branch from da4efca to 0e3625d Compare September 13, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FileReader Failure Causes Permanent Deadlock in Thumbnail Queue (_processThumbnailQueue` Stalls Indefinitely)

1 participant