Skip to content

Media Library reports a failed upload as a success #2282

Description

@Hridayesh13

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:

  1. The real error is never surfaced. uploadMedia() throws a useful message (File type not allowed, File exceeds maximum size of …) and it is discarded.
  2. 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

  1. Open the Media Library in the admin (/_emdash/admin/media).
  2. 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).
  3. Observe the green "File uploaded" banner, and that no item is added to the grid.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions