Add error handling improvements for upload-media#74917
Add error handling improvements for upload-media#74917adamsilverstein wants to merge 41 commits into
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
|
Size Change: +1.07 kB (+0.01%) Total Size: 8.21 MB 📦 View Changed
|
|
Flaky tests detected in 0f98395. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27035639982
|
22b6f13 to
34b509a
Compare
c5d67ea to
7d17226
Compare
There was a problem hiding this comment.
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
ErrorCodeenum 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.
1fb78d7 to
89e2a02
Compare
9e4d2bc to
05918ca
Compare
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.
There was a problem hiding this comment.
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 inErrorCode. RETRYABLE_CODEScontainsNETWORK_ERROR,TIMEOUT_ERROR,SERVER_ERROR,VIPS_WORKER_ERROR. Since none of those codes is ever thrown in the current code paths,upload-error.isRetryableeffectively always returnsfalse.getErrorMessage()/getRetryMessage()/getMaxRetriesExceededMessage()are not imported from anywhere, andErrorCode/ these helpers are not re-exported frompackages/upload-media/src/index.ts, so no consumer (this package or plugin code) can use them either.
Suggested direction before merge (pick one):
- Minimum: migrate all existing throw sites to use
ErrorCodevalues, addErrorCodeto 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 exportErrorCode+getErrorMessagefromindex.ts. Also typeUploadError.codeasErrorCode | stringinstead of rawstring. - Or: drop
error-messages.tsandErrorCodefrom 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'orglobalThis.SCRIPT_DEBUG, matching the@wordpress/warningpattern. 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.mdis 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
retryItemaction,retryCountfield on QueueItem, andRetryItemreducer/tests are all here (they actually landed in earlier trunk work, but the current description doesn't acknowledge that context). getRetryMessage()andgetMaxRetriesExceededMessage()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
finishUploadidempotency guard inuploadItem(preventing double-dispatch when bothonFileChangeandonSuccessfire) is a good incidental fix. Worth calling out in the description so it doesn't look like noise. @wordpress/warningcalls incancelItemare useful in dev builds; they only emit underSCRIPT_DEBUG, so no prod overhead. 👍- Sideload
measure()instrumentation plays nicely with the newonSuccess(subSize)trunk API after the merge.
Nits
error-messages.ts:getErrorMessagetakescode: string; once the enum is wired up, tightening that toErrorCode | stringwill catch typos at callsites.- The batch-completed
warning()fires inside thecancelItemfinishing 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.
Final sweep + suggested PR descriptionMerge conflict vs trunk is resolved and pushed ( CodeRabbit (run via Remaining items worth addressing before mergeSummarizing from my earlier review for visibility:
|
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
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)
ErrorCodeenum 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#isRetryablegetter backed by aRETRYABLE_CODESallowlist.validate-file-size,validate-mime-type,validate-mime-type-for-user,store/private-actions) migrated from bare string codes toErrorCodeenum members.error-messages.ts— localized, i18n-ready message map (title/description/ optionalaction) covering every enum code with a sane fallback.ErrorCode,getErrorMessage, and theErrorMessageConfigtype are now re-exported frompackages/upload-media/src/index.tsso consumers (Notice UI, block-editor) can render friendly messages off a rawUploadError.Dev-only logging via
@wordpress/warningcancelItemnow emits deduplicated warnings on silent cancels, missingonErrorhandlers, and batch completion.SCRIPT_DEBUG, so there's no production overhead.Performance instrumentation (User Timings API)
store/utils/debug-logger.tsexposes ameasure()helper that writes entries to a custom "Upload Media" track visible in the DevTools Performance panel.uploadItem,sideloadItem,resizeCropItem,rotateItem,transcodeImageItem.globalThis.SCRIPT_DEBUG(matching the@wordpress/warningpattern), so production builds skip theperformance.measurecall entirely.Incidental fix
uploadItemnow guards against double-dispatch offinishOperationwhen bothonFileChangeandonSuccessfire for the same attachment (idempotentfinishUploadclosure).Files
Modified
packages/upload-media/src/upload-error.ts—ErrorCodeenum +isRetryablegetterpackages/upload-media/src/validate-file-size.ts— useErrorCodepackages/upload-media/src/validate-mime-type.ts— useErrorCodepackages/upload-media/src/validate-mime-type-for-user.ts— useErrorCodepackages/upload-media/src/store/actions.ts—warning()calls incancelItempackages/upload-media/src/store/private-actions.ts—measure()instrumentation +finishUploadidempotency +ErrorCodepackages/upload-media/src/store/test/actions.ts— assertwarning()firespackages/upload-media/src/store/test/reducer.ts— incidental coverage forPauseQueue,ResumeQueue,CacheBlobUrl,RevokeBlobUrlspackages/upload-media/src/index.ts— exportErrorCode,getErrorMessage,ErrorMessageConfigpackages/upload-media/package.json— add@wordpress/warningdeppackages/upload-media/tsconfig.json— addwarningproject referencepackages/upload-media/CHANGELOG.md— entries under Unreleased.gitignore— ignoretest-results/Added
packages/upload-media/src/error-messages.ts— localized message map +getErrorMessage()packages/upload-media/src/store/utils/debug-logger.ts—measure()helperTest plan
@wordpress/upload-media)tsc --noEmitclean across the packageprettier --checkclean on all changed filesSCRIPT_DEBUG = true, verify@wordpress/warningsurfaces on cancel and batch completionSCRIPT_DEBUG = true, confirm "Upload Media" track appears in DevTools Performance panel for an upload with sub-size sideloadsmaxUploadFileSize, verify the resultingUploadErrorpassed toonErrorhascode === ErrorCode.SIZE_ABOVE_LIMITandisRetryable === falseOut of scope (follow-up PR)
scheduleRetry,executeRetry, retry-timer map). The existing manualretryItemaction,retryCountfield, andRetryItemreducer (all pre-dating this PR) remain ontrunkand are unchanged here.NETWORK_ERROR/TIMEOUT_ERROR/SERVER_ERRORsoisRetryablehas real producers. The enum is already in place for when that lands.getErrorMessage().