Description
The Media Library reports a failed upload as a success. The green "File uploaded" banner appears and nothing is added to the list, so the operator is told the upload worked while it silently did not.
MediaPage passes mutate as the upload handler (packages/admin/src/router.tsx:1344 on main):
onUpload={(file) => uploadMutation.mutate(file)}
mutate() is fire-and-forget: it returns undefined and never rejects. But MediaLibrary.handleFileSelect awaits that return value inside a try/catch and derives the result from whether it threw:
for (const file of fileArray) {
try {
await onUpload?.(file);
uploaded++;
} catch (error) {
console.error("Upload failed:", error);
failed++;
}
...
}
if (failed === 0) setUploadState({ status: "success", message: /* "File uploaded" */ });
Since the awaited value is undefined and nothing ever throws, failed stays 0 and every upload — including a rejected one — takes the success branch.
Two consequences that make this hard to diagnose in the field:
- The real error is never surfaced.
uploadMedia() throws a useful message (File type not allowed, File exceeds maximum size of …) and it is discarded.
- The banner auto-clears after 3s (the
uploadState effect in MediaLibrary), so even the misleading message is gone by the time a user looks.
Expected: a rejected upload shows the error state, ideally naming the reason the API returned.
This cost us a lot of time downstream — QA reported "uploaded images don't appear in the list", which read as a broken upload pipeline. The pipeline was fine; the UI was reporting failure as success.
Steps to reproduce
- Open the Media Library in the admin (
/_emdash/admin/media).
- Click Upload and choose a file the API rejects — a
.txt file is the simplest (POST /_emdash/api/media answers 400 INVALID_TYPE; the same happens for a file over maxUploadSize, which returns 413).
- Observe the green "File uploaded" banner, and that no item is added to the grid.
- Sample within ~1.2s — the banner auto-clears at 3s, so a slow look sees no feedback at all.
The network tab shows the request failing while the UI reports success.
Environment
- emdash version: main @ 668184f (also present in published 0.31.1; originally found on 0.8.0)
- Node.js version: 22
- Runtime: Cloudflare Workers (R2 binding); the faulty branch is runtime-independent
- OS: macOS host / Linux container
The browser reproduction described above was performed on 0.8.0. Against current main I verified it differently, by adding a test to packages/admin/tests/ that asserts onUpload rejects when the upload fails: on unmodified main it fails with
AssertionError: promise resolved "undefined" instead of rejecting
which is this bug stated directly.
Logs / error output
# What the API returns (correctly):
POST /_emdash/api/media -> 400
{"error":{"code":"INVALID_TYPE","message":"File type not allowed"}}
# What the admin UI displays:
✅ File uploaded
A PR with the fix and that regression test is attached.
Reported with AI assistance (Claude Opus 5). The reporter is responsible for correctness.
Description
The Media Library reports a failed upload as a success. The green "File uploaded" banner appears and nothing is added to the list, so the operator is told the upload worked while it silently did not.
MediaPagepassesmutateas the upload handler (packages/admin/src/router.tsx:1344onmain):mutate()is fire-and-forget: it returnsundefinedand never rejects. ButMediaLibrary.handleFileSelectawaits that return value inside atry/catchand derives the result from whether it threw:Since the awaited value is
undefinedand nothing ever throws,failedstays0and every upload — including a rejected one — takes the success branch.Two consequences that make this hard to diagnose in the field:
uploadMedia()throws a useful message (File type not allowed,File exceeds maximum size of …) and it is discarded.uploadStateeffect inMediaLibrary), so even the misleading message is gone by the time a user looks.Expected: a rejected upload shows the error state, ideally naming the reason the API returned.
This cost us a lot of time downstream — QA reported "uploaded images don't appear in the list", which read as a broken upload pipeline. The pipeline was fine; the UI was reporting failure as success.
Steps to reproduce
/_emdash/admin/media)..txtfile is the simplest (POST /_emdash/api/mediaanswers400 INVALID_TYPE; the same happens for a file overmaxUploadSize, which returns413).The network tab shows the request failing while the UI reports success.
Environment
The browser reproduction described above was performed on 0.8.0. Against current
mainI verified it differently, by adding a test topackages/admin/tests/that assertsonUploadrejects when the upload fails: on unmodifiedmainit fails withwhich is this bug stated directly.
Logs / error output
A PR with the fix and that regression test is attached.
Reported with AI assistance (Claude Opus 5). The reporter is responsible for correctness.