Skip to content

Add error handling improvements for upload-media#74917

Open
adamsilverstein wants to merge 41 commits into
trunkfrom
74366-add-retry-and-error-handling
Open

Add error handling improvements for upload-media#74917
adamsilverstein wants to merge 41 commits into
trunkfrom
74366-add-retry-and-error-handling

Conversation

@adamsilverstein
Copy link
Copy Markdown
Member

@adamsilverstein adamsilverstein commented Jan 23, 2026

Fixes #74366

Summary

Adds error classification + logging + instrumentation to @wordpress/upload-media. Split from the original combined PR per reviewer feedback — auto-retry/scheduling lands in a follow-up PR.

What's in this PR

Error classification (wired up)

  • ErrorCode enum covering every code currently thrown by the package (EMPTY_FILE, SIZE_ABOVE_LIMIT, MIME_TYPE_NOT_SUPPORTED, MIME_TYPE_NOT_ALLOWED_FOR_USER, HEIC_DECODE_ERROR, IMAGE_TRANSCODING_ERROR, IMAGE_ROTATION_ERROR, MEDIA_TRANSCODING_ERROR), plus aspirational retry-targets (NETWORK_ERROR, TIMEOUT_ERROR, SERVER_ERROR) kept for the retry follow-up.
  • UploadError#isRetryable getter backed by a RETRYABLE_CODES allowlist.
  • All existing throw sites (validate-file-size, validate-mime-type, validate-mime-type-for-user, store/private-actions) migrated from bare string codes to ErrorCode enum members.
  • error-messages.ts — localized, i18n-ready message map (title / description / optional action) covering every enum code with a sane fallback.
  • ErrorCode, getErrorMessage, and the ErrorMessageConfig type are now re-exported from packages/upload-media/src/index.ts so consumers (Notice UI, block-editor) can render friendly messages off a raw UploadError.

Dev-only logging via @wordpress/warning

  • cancelItem now emits deduplicated warnings on silent cancels, missing onError handlers, and batch completion.
  • Only fires under SCRIPT_DEBUG, so there's no production overhead.

Performance instrumentation (User Timings API)

  • store/utils/debug-logger.ts exposes a measure() helper that writes entries to a custom "Upload Media" track visible in the DevTools Performance panel.
  • Instrumented: uploadItem, sideloadItem, resizeCropItem, rotateItem, transcodeImageItem.
  • Gated on globalThis.SCRIPT_DEBUG (matching the @wordpress/warning pattern), so production builds skip the performance.measure call entirely.

Incidental fix

  • uploadItem now guards against double-dispatch of finishOperation when both onFileChange and onSuccess fire for the same attachment (idempotent finishUpload closure).

Files

Modified

  • packages/upload-media/src/upload-error.tsErrorCode enum + isRetryable getter
  • packages/upload-media/src/validate-file-size.ts — use ErrorCode
  • packages/upload-media/src/validate-mime-type.ts — use ErrorCode
  • packages/upload-media/src/validate-mime-type-for-user.ts — use ErrorCode
  • packages/upload-media/src/store/actions.tswarning() calls in cancelItem
  • packages/upload-media/src/store/private-actions.tsmeasure() instrumentation + finishUpload idempotency + ErrorCode
  • packages/upload-media/src/store/test/actions.ts — assert warning() fires
  • packages/upload-media/src/store/test/reducer.ts — incidental coverage for PauseQueue, ResumeQueue, CacheBlobUrl, RevokeBlobUrls
  • packages/upload-media/src/index.ts — export ErrorCode, getErrorMessage, ErrorMessageConfig
  • packages/upload-media/package.json — add @wordpress/warning dep
  • packages/upload-media/tsconfig.json — add warning project reference
  • packages/upload-media/CHANGELOG.md — entries under Unreleased
  • .gitignore — ignore test-results/

Added

  • packages/upload-media/src/error-messages.ts — localized message map + getErrorMessage()
  • packages/upload-media/src/store/utils/debug-logger.tsmeasure() helper

Test plan

  • All existing unit tests pass (188 tests across @wordpress/upload-media)
  • tsc --noEmit clean across the package
  • prettier --check clean on all changed files
  • Manual: upload with SCRIPT_DEBUG = true, verify @wordpress/warning surfaces on cancel and batch completion
  • Manual: with SCRIPT_DEBUG = true, confirm "Upload Media" track appears in DevTools Performance panel for an upload with sub-size sideloads
  • Manual: upload a file over maxUploadFileSize, verify the resulting UploadError passed to onError has code === ErrorCode.SIZE_ABOVE_LIMIT and isRetryable === false

Out of scope (follow-up PR)

  • Automatic retry scheduling (scheduleRetry, executeRetry, retry-timer map). The existing manual retryItem action, retryCount field, and RetryItem reducer (all pre-dating this PR) remain on trunk and are unchanged here.
  • Wrapping network/fetch failures as NETWORK_ERROR / TIMEOUT_ERROR / SERVER_ERROR so isRetryable has real producers. The enum is already in place for when that lands.
  • Consumer UX (Notice / upload list row) that renders getErrorMessage().

@github-actions

This comment was marked as outdated.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jan 23, 2026

Size Change: +1.07 kB (+0.01%)

Total Size: 8.21 MB

📦 View Changed
Filename Size Change
build/scripts/upload-media/index.min.js 13.2 kB +1.07 kB (+8.79%) 🔍

compressed-size-action

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jan 24, 2026

Flaky tests detected in 0f98395.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27035639982
📝 Reported issues:

@adamsilverstein adamsilverstein marked this pull request as draft January 24, 2026 02:13
@adamsilverstein adamsilverstein force-pushed the 74366-add-retry-and-error-handling branch from 22b6f13 to 34b509a Compare January 24, 2026 02:17
@github-actions github-actions Bot added [Package] Block library /packages/block-library [Package] Block editor /packages/block-editor labels Jan 24, 2026
@adamsilverstein adamsilverstein force-pushed the 74366-add-retry-and-error-handling branch from c5d67ea to 7d17226 Compare January 24, 2026 02:26
@github-actions github-actions Bot removed [Package] Core data /packages/core-data [Package] Block library /packages/block-library [Package] Block editor /packages/block-editor [Package] Sync labels Jan 24, 2026
@adamsilverstein adamsilverstein requested review from Copilot and removed request for nerrad January 24, 2026 05:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements comprehensive error handling and retry logic for the upload-media package to address GitHub Issue #74366. The implementation adds an error classification system with 16 error types, automatic retry with exponential backoff for transient failures, user-friendly localized error messages, and retry state management with a new ItemStatus.PendingRetry status.

Changes:

  • Added ErrorCode enum with 16 error types categorized as retryable (network, timeout, server, VIPS worker errors) and non-retryable (validation, permission, file size, MIME type errors)
  • Implemented automatic retry with exponential backoff and jitter for failed uploads, with configurable retry settings (default: 3 attempts, 1s-30s delays, 2x multiplier)
  • Created comprehensive error message system with localized, actionable user guidance for all error types

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/upload-media/src/upload-error.ts Added ErrorCode enum and isRetryable getter to classify errors
packages/upload-media/src/error-messages.ts New file providing user-friendly i18n error messages for all error codes
packages/upload-media/src/store/utils/retry.ts New file with exponential backoff calculation and error classification logic
packages/upload-media/src/store/utils/debug-logger.ts Added logging functions for retry scheduling, execution, and max retries exceeded
packages/upload-media/src/store/types.ts Added RetrySettings, ScheduleRetryAction, ItemStatus.PendingRetry, and retry-related QueueItem fields
packages/upload-media/src/store/constants.ts Added retry configuration constants (max attempts, delays, multiplier, jitter)
packages/upload-media/src/store/reducer.ts Added ScheduleRetry case and default retry settings initialization
packages/upload-media/src/store/actions.ts Modified cancelItem to check retry eligibility, added scheduleRetry and executeRetry actions
packages/upload-media/src/store/private-selectors.ts Added selectors for retry state (pending items, retry timestamp, retry count, max retries check)
packages/upload-media/src/store/test/retry.ts New test file with 28 unit tests for retry utilities
packages/upload-media/src/index.ts Exported ErrorCode, error message functions, and retry-related types
packages/upload-media/README.md Updated documentation for cancelItem, scheduleRetry, and executeRetry actions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/upload-media/src/store/actions.ts Outdated
Comment thread packages/upload-media/src/store/types.ts Outdated
Comment thread packages/upload-media/src/store/reducer.ts
Comment thread packages/upload-media/src/store/actions.ts
Comment thread packages/upload-media/src/index.ts Outdated
Comment thread packages/upload-media/src/store/actions.ts Outdated
@adamsilverstein adamsilverstein linked an issue Jan 29, 2026 that may be closed by this pull request
5 tasks
@adamsilverstein adamsilverstein force-pushed the feature/client-side-media-dev branch from 1fb78d7 to 89e2a02 Compare February 2, 2026 20:04
@adamsilverstein adamsilverstein force-pushed the 74366-add-retry-and-error-handling branch 2 times, most recently from 9e4d2bc to 05918ca Compare February 3, 2026 19:06
@adamsilverstein adamsilverstein added [Type] Enhancement A suggestion for improvement. [Feature] Client Side Media Media processing in the browser with WASM [Status] In Progress Tracking issues with work in progress labels Feb 24, 2026
@adamsilverstein adamsilverstein self-assigned this Feb 24, 2026
@adamsilverstein adamsilverstein changed the base branch from feature/client-side-media-dev to trunk February 24, 2026 09:15
adamsilverstein and others added 2 commits February 24, 2026 16:31
Implements automatic retry with exponential backoff for failed uploads,
error classification system, and user-friendly error messages.

Changes:
- Add ErrorCode enum with 16 error types (network, validation, processing)
- Add isRetryable getter to UploadError class for automatic retry classification
- Add retry configuration constants (max attempts, delays, backoff multiplier)
- Add RetrySettings interface and ItemStatus.PendingRetry status
- Add scheduleRetry/executeRetry actions for automatic retry scheduling
- Add retry state selectors (getPendingRetryItems, getItemRetryCount, etc.)
- Add retry logging functions for debugging
- Add calculateRetryDelay utility with exponential backoff and jitter
- Add shouldRetryError utility for error classification
- Add user-friendly i18n error messages for all error codes
- Add comprehensive unit tests for retry utilities (28 tests)

The retry system automatically retries failed uploads for transient errors
(network failures, timeouts, server errors) while immediately failing for
non-retryable errors (validation, permissions, file size limits).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Resolve conflict in sideloadItem: keep measure() instrumentation while
adopting trunk's new onSuccess(subSize) callback that accumulates
SubSizeData onto the parent item.
Copy link
Copy Markdown
Member Author

@adamsilverstein adamsilverstein left a comment

Choose a reason for hiding this comment

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

Review from Claude

I took a pass through the PR after resolving the latest trunk merge conflict. A few substantive issues to flag before this lands, plus a couple of cleanups.

🔴 Error-handling infrastructure is not wired into the code it's meant to handle

The new ErrorCode enum + error-messages.ts + isRetryable getter form a nice vocabulary, but nothing in the package actually produces those codes, and nothing consumes the helpers:

  • Actual new UploadError( { code: … } ) sites use string codes like 'HEIC_DECODE_ERROR', 'EMPTY_FILE', 'SIZE_ABOVE_LIMIT', 'MIME_TYPE_NOT_ALLOWED_FOR_USER', 'MIME_TYPE_NOT_SUPPORTED', 'MEDIA_TRANSCODING_ERROR', 'IMAGE_TRANSCODING_ERROR', 'IMAGE_ROTATION_ERROR' — none of which appear in ErrorCode.
  • RETRYABLE_CODES contains NETWORK_ERROR, TIMEOUT_ERROR, SERVER_ERROR, VIPS_WORKER_ERROR. Since none of those codes is ever thrown in the current code paths, upload-error.isRetryable effectively always returns false.
  • getErrorMessage() / getRetryMessage() / getMaxRetriesExceededMessage() are not imported from anywhere, and ErrorCode / these helpers are not re-exported from packages/upload-media/src/index.ts, so no consumer (this package or plugin code) can use them either.

Suggested direction before merge (pick one):

  1. Minimum: migrate all existing throw sites to use ErrorCode values, add ErrorCode to the enum for the remaining legacy codes (HEIC_DECODE_ERROR, EMPTY_FILE, SIZE_ABOVE_LIMIT, MIME_TYPE_NOT_ALLOWED_FOR_USER, MIME_TYPE_NOT_SUPPORTED, MEDIA_TRANSCODING_ERROR), and export ErrorCode + getErrorMessage from index.ts. Also type UploadError.code as ErrorCode | string instead of raw string.
  2. Or: drop error-messages.ts and ErrorCode from this PR and land them alongside the UI that consumes them. Otherwise CI will happily ship dead code.

🟡 debug-logger.ts is a compile-time no-op

DEBUG_ENABLED = false is a module-level constant, so measure() always short-circuits in every build. If the goal is to leave hooks in the code for people to turn on during local work, that's fine — but it's worth being explicit:

  • Gate it with process.env.NODE_ENV === 'development' or globalThis.SCRIPT_DEBUG, matching the @wordpress/warning pattern. Or
  • Expose a setDebugEnabled() / read from a well-known global so it can be flipped without editing the source.

Otherwise a future reader has to grep the file to find the constant and rebuild the package to use it.

🟡 PR description is stale

  • README.md is listed as modified, but the diff shows no README change.
  • "Retry logic will be handled in a separate PR" — true for auto-retry/scheduling, but the user-initiated retryItem action, retryCount field on QueueItem, and RetryItem reducer/tests are all here (they actually landed in earlier trunk work, but the current description doesn't acknowledge that context).
  • getRetryMessage() and getMaxRetriesExceededMessage() shipped with this PR but are scoped to the retry UX that's explicitly not in this PR.

Description should reflect: "adds error-handling vocabulary + logging instrumentation, no consumer wiring yet (follow-up PR)" — or you wire up the consumer and update the description accordingly.

🟡 test-results/.last-run.json is accidentally committed

Already flagged by @andrewserong. Still in the branch:

 test-results/.last-run.json                        |  10 +

Please remove and add test-results/ to .gitignore (currently only test/storybook-playwright/test-results is ignored).

🟡 Unrelated test additions

packages/upload-media/src/store/test/reducer.ts adds tests for PauseQueue, ResumeQueue, CacheBlobUrl, RevokeBlobUrls — which are good tests, but none are touched by this PR. If the intent is "improve reducer coverage while I'm here," that's reasonable to keep, but mention it in the description. Otherwise consider splitting them out so this PR stays focused.

🟡 No CHANGELOG entry

packages/upload-media/CHANGELOG.md has an Unreleased entry from #75257 but nothing for this PR. Add a bullet.

🟢 Nice touches

  • The finishUpload idempotency guard in uploadItem (preventing double-dispatch when both onFileChange and onSuccess fire) is a good incidental fix. Worth calling out in the description so it doesn't look like noise.
  • @wordpress/warning calls in cancelItem are useful in dev builds; they only emit under SCRIPT_DEBUG, so no prod overhead. 👍
  • Sideload measure() instrumentation plays nicely with the new onSuccess(subSize) trunk API after the merge.

Nits

  • error-messages.ts: getErrorMessage takes code: string; once the enum is wired up, tightening that to ErrorCode | string will catch typos at callsites.
  • The batch-completed warning() fires inside the cancelItem finishing branch, which means a silent cancel of the last item in a batch still logs "Batch completed". If that's intentional, fine, but worth a comment.

@adamsilverstein
Copy link
Copy Markdown
Member Author

adamsilverstein commented Apr 21, 2026

Final sweep + suggested PR description

Merge conflict vs trunk is resolved and pushed (21a8c0c5980) — the sideload change now uses trunk's onSuccess(subSize) callback and keeps the new measure() timing. All 188 upload-media unit tests pass locally.

CodeRabbit (run via coderabbit review --plain --base origin/trunk) returned no findings against this PR's diff.

Remaining items worth addressing before merge

Summarizing from my earlier review for visibility:

  1. Wire up ErrorCode or drop it. The enum + isRetryable + error-messages.ts currently have no producers and no consumers. Either migrate existing throw sites to the enum and export from index.ts, or defer this file to the follow-up PR that actually consumes it. Shipping as-is means dead code in npm for one release cycle.
  2. Delete test-results/.last-run.json (already flagged by @andrewserong) and consider adding test-results/ to .gitignore. The file is still in the diff.
  3. debug-logger.ts gate. DEBUG_ENABLED = false is a compile-time constant — measure() is always a no-op unless someone edits the file. If the goal is "dev-only instrumentation," gate on process.env.NODE_ENV === 'development' or globalThis.SCRIPT_DEBUG (matching @wordpress/warning); otherwise document that this is an opt-in developer tool.
  4. Add a CHANGELOG.md entry under the existing ## Unreleased section of packages/upload-media/CHANGELOG.md.
  5. Retry helpersgetRetryMessage() / getMaxRetriesExceededMessage() are for the retry UX you've said is out of scope. If the follow-up PR is imminent, leaving them is fine; otherwise consider removing them to keep this PR focused on error classification + logging.

adamsilverstein and others added 10 commits April 21, 2026 11:10
Delete test-results/.last-run.json (Playwright run artifact) and add
test-results/ to .gitignore so it won't be committed again.
Replace DEBUG_ENABLED = false with a runtime check of globalThis.SCRIPT_DEBUG,
matching the @wordpress/warning pattern. Production builds become no-ops
automatically, developers can flip SCRIPT_DEBUG to see the Upload Media
track in the DevTools Performance panel without editing the source.
Drop getRetryMessage() and getMaxRetriesExceededMessage() — these
belong with the retry scheduling work shipping in a follow-up PR,
not the error-classification scaffolding.
Previously the ErrorCode enum and error-messages.ts helpers were
scaffolding with no producers or consumers: throw sites used bare
strings ('HEIC_DECODE_ERROR', 'EMPTY_FILE', etc.), isRetryable
always returned false, and nothing was exported from index.ts.

Changes:
- Rework ErrorCode to include the codes actually thrown today
  (EMPTY_FILE, SIZE_ABOVE_LIMIT, MIME_TYPE_NOT_SUPPORTED,
  MIME_TYPE_NOT_ALLOWED_FOR_USER, HEIC_DECODE_ERROR,
  IMAGE_TRANSCODING_ERROR, IMAGE_ROTATION_ERROR,
  MEDIA_TRANSCODING_ERROR) plus retry-targets kept for the
  follow-up PR (NETWORK_ERROR, TIMEOUT_ERROR, SERVER_ERROR).
- Drop speculative codes that nothing threw or consumed.
- Migrate validate-file-size, validate-mime-type,
  validate-mime-type-for-user, and private-actions throw sites
  to use ErrorCode members instead of string literals.
- Rewrite error-messages.ts so every enum entry has a matching
  localized message, and tighten getErrorMessage's parameter
  type to ErrorCode | string.
- Export ErrorCode, getErrorMessage, and ErrorMessageConfig
  from the package index so consumers can actually use them.
Resolve conflicts in packages/upload-media/package.json and
package-lock.json by keeping the new @wordpress/warning dependency
introduced in this PR alongside trunk's uuid bump to ^14.0.0.

Move the PR's changelog entries back under the Unreleased heading
since this PR is not yet merged; trunk's 0.30.0 release contains
only the #75257 entry.
Resolve conflicts in upload-media CHANGELOG and store actions:
- CHANGELOG: combine Unreleased entries from both sides; keep trunk's
  scaled-suffix bug fixes alongside this PR's enhancements.
- actions.ts: keep trunk's parentId guard on the cancellation logger and
  layer this PR's warning() call inside it; adopt trunk's local batchId
  variable in the batch-complete check.
- private-actions.ts: keep this PR's ErrorCode enum members on the
  IMAGE_TRANSCODING_ERROR / IMAGE_ROTATION_ERROR throws while adopting
  trunk's localized user-facing messages.
…error-handling

# Conflicts:
#	packages/upload-media/CHANGELOG.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Client Side Media Media processing in the browser with WASM Needs Testing Needs further testing to be confirmed. [Status] In Progress Tracking issues with work in progress [Type] Enhancement A suggestion for improvement.

Projects

Status: 🔎 Needs Review

Development

Successfully merging this pull request may close these issues.

Add comprehensive error handling

4 participants