Skip to content

fix(admin): report failed media uploads as failures - #2283

Closed
Hridayesh13 wants to merge 2 commits into
emdash-cms:mainfrom
Hridayesh13:fix/media-upload-failure-reporting
Closed

fix(admin): report failed media uploads as failures#2283
Hridayesh13 wants to merge 2 commits into
emdash-cms:mainfrom
Hridayesh13:fix/media-upload-failure-reporting

Conversation

@Hridayesh13

Copy link
Copy Markdown

What does this PR do?

MediaPage passed uploadMutation.mutate as MediaLibrary's onUpload. MediaLibrary awaits that call and counts rejections to decide between the success and the error banner — but mutate() is fire-and-forget: it returns undefined and never rejects. Every upload was therefore scored a success, so a rejected upload rendered the green "File uploaded" banner while nothing appeared in the list, and the reason the API gave (File type not allowed, File exceeds maximum size of …) was discarded.

The fix is to await mutateAsync so the rejection reaches the caller that is already written to handle it. No change to MediaLibrary itself.

This is worth more than the one-line diff suggests: with the banner lying and auto-clearing after 3s, a misconfigured install is indistinguishable from a working one. It sent our QA down a multi-day hunt for a broken upload pipeline that was never broken.

Closes #2282

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable) — no new strings; the existing error path is already wrapped
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion — n/a, bug fix

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 5 (Claude Code)

Screenshots / test output

packages/admin/tests/media-upload-failure.test.tsx asserts that onUpload rejects when the upload fails. It detects a revert — with the fix backed out and everything else unchanged:

FAIL  chromium  tests/media-upload-failure.test.tsx > MediaPage upload failure reporting
  > rejects when the upload fails, so MediaLibrary can show the error
AssertionError: promise resolved "undefined" instead of rejecting

With the fix applied:

✓ chromium  tests/media-upload-failure.test.tsx (2 tests) 166ms
  Test Files  1 passed (1)
       Tests  2 passed (2)

Full suites on this branch's base (668184f):

packages/core   390 passed | 1 skipped (391 files), 5024 passed | 3 skipped
packages/admin  105 passed (105 files), 1241 passed
pnpm typecheck  packages/admin: Done
pnpm format     clean (no files changed outside this PR)

Two notes on the shared gates, both reproduced on a clean checkout of main @ 668184f with no changes applied, i.e. pre-existing and unrelated to this PR:

  • pnpm typecheck fails in packages/coresrc/plugins/context.ts(1212,24): error TS2345: Argument of type '"cache:purge"' is not assignable to parameter of type 'PluginCapability'.
  • pnpm lint reports 1 warning (and --deny-warnings turns it into a failure) — typescript(no-unnecessary-type-assertion) at packages/cloudflare/src/sandbox/bridge.ts:1210.

Happy to rebase once those are sorted if it makes CI easier to read.

MediaPage passed `uploadMutation.mutate` as MediaLibrary's `onUpload`.
MediaLibrary awaits that call and counts rejections to choose between the
success and the error banner, but `mutate()` is fire-and-forget: it
returns undefined and never rejects. Every upload was therefore scored a
success, so a rejected upload rendered the green "File uploaded" banner
while nothing appeared in the list and the API's reason was discarded --
leaving no way to tell a broken configuration from a working one.

Await `mutateAsync` instead so the rejection reaches the caller.
@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e0bc43d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet area/admin size/M labels Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@Hridayesh13

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Jul 30, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is the right change for the reported bug. MediaLibrary already awaits onUpload and counts rejections to choose between the success and error banner, but mutate() is fire-and-forget, so the rejection from uploadMedia never reached it. Switching the MediaPage wiring to mutateAsync propagates the error with a minimal, well-scoped source change.

I reviewed statically — no tests, build, or install were run. Checks made:

  • Confirmed MediaLibrary.handleFileSelect awaits onUpload and increments failed only on rejection.
  • Verified the other on* mutation handlers in router.tsx (e.g., onDelete, onSave) pass .mutate to components that drive their own pending/error UI, so this fire-and-forget pattern isn't a systemic issue elsewhere.
  • Confirmed the new test covers both the rejection path and the happy path, and fails if the handler is reverted to mutate.
  • No new admin UI strings, SQL, server routes, or logged-out queries are introduced. The changeset is present and correctly targets @emdash-cms/admin.

The only real issue is a comment-discipline violation in the source: the five-line inline comment above onUpload is PR-description prose that restates the bug and justifies the implementation choice. AGENTS.md says comments should not summarize the change or justify decisions; that context belongs in the changeset and commit message. The code and type signature already make the contract obvious.

I also noticed the new test file carries a similarly verbose opening docblock and a couple of inline justifying comments; these should be trimmed for the same reason, but they don't affect behavior.

No blocking correctness, security, or performance issues.

Comment thread packages/admin/src/router.tsx Outdated
Comment on lines +1344 to +1348
// mutateAsync, not mutate: MediaLibrary awaits this call and counts
// rejections to decide between the success and error banner. mutate()
// never rejects, so a failed upload was scored as a success -- the
// green "File uploaded" banner appeared and nothing was added to the
// list, with the API's reason discarded.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] This five-line comment is PR-description prose: it restates the bug, names the green banner, and justifies choosing mutateAsync over mutate. AGENTS.md prohibits comments that summarize the change or justify a decision — that context belongs in the changeset and commit message. The wrapper itself already makes the await/reject contract clear. Remove the comment block.

Suggested change
// mutateAsync, not mutate: MediaLibrary awaits this call and counts
// rejections to decide between the success and error banner. mutate()
// never rejects, so a failed upload was scored as a success -- the
// green "File uploaded" banner appeared and nothing was added to the
// list, with the API's reason discarded.
onLoadMore={() => void fetchNextPage()}
onUpload={async (file) => {
await uploadMutation.mutateAsync(file);
}}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e0bc43d — comment block removed, wrapper left as-is.

Comment on lines +2 to +22
/**
* MediaPage must surface a failed upload as a failure.
*
* `MediaLibrary.handleFileSelect` awaits the `onUpload` prop and decides between
* the success and error banner by counting rejections:
*
* try { await onUpload?.(file); uploaded++ }
* catch { failed++ }
* ...
* if (failed === 0) setUploadState({ status: "success", ... })
*
* MediaPage used to pass `uploadMutation.mutate`, which is fire-and-forget — it
* returns `undefined` and never rejects. So `failed` stayed 0 no matter what the
* API said, and a rejected upload rendered the green "File uploaded" banner
* while nothing was added to the list, with the API's reason discarded. Passing
* `mutateAsync` propagates the real error to that catch.
*
* The assertion is on the prop contract rather than the rendered banner: the
* banner is MediaLibrary's behaviour (and is already correct), whereas the bug
* was purely in how MediaPage wired the handler.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The opening docblock is a condensed bug narrative rather than a test-purpose comment. AGENTS.md discourages comments that read like PR descriptions. The test name and code already explain what is being verified. Trim or remove this block, and consider deleting the inline justifying comments on lines 120 and 140 as well.

Suggested change
/**
* MediaPage must surface a failed upload as a failure.
*
* `MediaLibrary.handleFileSelect` awaits the `onUpload` prop and decides between
* the success and error banner by counting rejections:
*
* try { await onUpload?.(file); uploaded++ }
* catch { failed++ }
* ...
* if (failed === 0) setUploadState({ status: "success", ... })
*
* MediaPage used to pass `uploadMutation.mutate`, which is fire-and-forget it
* returns `undefined` and never rejects. So `failed` stayed 0 no matter what the
* API said, and a rejected upload rendered the green "File uploaded" banner
* while nothing was added to the list, with the API's reason discarded. Passing
* `mutateAsync` propagates the real error to that catch.
*
* The assertion is on the prop contract rather than the rendered banner: the
* banner is MediaLibrary's behaviour (and is already correct), whereas the bug
* was purely in how MediaPage wired the handler.
*/
import { Toasty } from "@cloudflare/kumo";
import { i18n } from "@lingui/core";

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e0bc43d — docblock removed, along with the two inline justifying comments you flagged.

AGENTS.md scopes comments to a future reader of the code: not PR
descriptions, not justification for a decision, not narrative about what
was tried. The blocks added with the fix were all three. The await/reject
contract is legible from the wrapper itself and the rest lives in the
commit message and changeset.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Jul 30, 2026
@Hridayesh13

Copy link
Copy Markdown
Author

Closing this — #2273 landed while it was in review and already carries the fix.

packages/admin/src/router.tsx on main is now byte-identical to this branch's head (await uploadMutation.mutateAsync(file)), so the source change here is a no-op. Git auto-resolves the identical edit, which is why it still shows as mergeable, but there is nothing left in it worth merging.

The only thing this PR still adds is packages/admin/tests/media-upload-failure.test.tsx, a regression test pinning that MediaPage propagates the upload rejection to MediaLibrary. #2273's new packages/admin/tests/lib/media-upload.test.ts covers dedup and hashing, not the router wiring, so that coverage is still absent — but it's not worth keeping this PR open for on its own.

Thanks for the review passes; the comment-discipline notes were fair and I applied them in e0bc43d. Happy to put the test up as a standalone PR if you'd want it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/admin cla: signed review/needs-rereview Author pushed changes since the last review size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Media Library reports a failed upload as a success

1 participant