Skip to content

Core Data: Discard entity record responses superseded by a newer request - #81846

Draft
adamsilverstein wants to merge 5 commits into
trunkfrom
fix/image-resolution-stale-attachment-record
Draft

Core Data: Discard entity record responses superseded by a newer request#81846
adamsilverstein wants to merge 5 commits into
trunkfrom
fix/image-resolution-stale-attachment-record

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Aug 20, 2026

Copy link
Copy Markdown
Member

Test this PR in WordPress Playground

Claude investigated the report and wrote the fix and tests:

Fixes #81844

What?

getEntityRecord now discards a REST response once a newer response for the same request has already been received.

An earlier revision of this PR fixed the reported symptom inside the Image block. Following @swissspidy's review that approach is gone, and the fix moved to the shared layer where the defect actually lives.

Why?

invalidateResolution starts a second request for a record while the first one may still be in flight, and the store keeps whichever response is delivered last. A response whose body was read from the database before an update can therefore overwrite the updated record - and once that happens nothing is left to invalidate.

Client-side media uploads hit this. The attachment is created first, its sub-sizes are sideloaded, and only the final finalize request writes media_details.sizes. The Image block starts resolving getEntityRecord( 'postType', 'attachment', id, { context: 'view' } ) as soon as the attachment ID lands in its attributes, so that GET returns "sizes": {}. On a busy server it can be delivered after the post-upload invalidation refetch, replacing the finalized record with the empty one - imageSizeOptions stays empty and the Resolution control never renders again. That matches the field evidence in the issue: a successful GET /wp/v2/media/{id}?context=view whose body contains "sizes": {}, and a front end whose srcset proves every sub-size was generated.

Playground serializes PHP requests, so responses cannot arrive out of order there - which is why the issue only reproduces on real installations.

Existing invalidation cannot fix this. mediaUploadOnSuccess already invalidates the attachment record centrally after an upload, but the stale response arrives after that invalidation has been acted on. The ordering is the bug.

How?

Each record request reserves a sequence number before it goes out. When its response comes back, it is dropped if a newer response for the same request has already been written to the store.

Two properties keep this conservative:

  • Only identical requests are compared. A request for a different set of _fields, or a different context, never discards another one's data.
  • A newer request that fails discards nothing. The guard trips only once a newer response has actually been received, so a failed refetch still leaves the older response to be used exactly as before.

Fixing it here means every consumer benefits - Gallery, Cover, Media & Text and the media modal read the same records and could hit the same stale-clobber.

Testing Instructions

Two tests, both verified to fail without the change and pass with it.

Unit - the ordering guard in isolation:

npm run test:unit -- packages/core-data/src/test/resolvers.js -t "ignores a response delivered"

E2e - the reported bug, reproduced deterministically. It holds the response of the attachment GET issued before finalize (reading its stale body immediately), delivers it only after the finalized record has been cached, then checks that the cached record keeps its sizes and the Resolution control is still visible:

npm run test:e2e -- test/e2e/specs/editor/various/client-side-media-processing.spec.js -g "stale attachment"

Manual reproduction needs an unlucky response ordering, so a fast local server usually will not show it - the linked issue has the field evidence.

Follow-up

getEntityRecords has the same last-delivered-wins behaviour for lists, and matters most for batch uploads. Left alone here to keep this change reviewable, tracked separately in #81885.

AI Use

Code and description both written with 🤖 Claude Code. I will review and test.

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>
Co-authored-by: swissspidy <swissspidy@git.wordpress.org>
Co-authored-by: gregbenz <gregbenz@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions github-actions Bot added the [Package] Block library /packages/block-library label Aug 20, 2026
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

Size Change: +67 B (0%)

Total Size: 7.91 MB

📦 View Changed
Filename Size Change
build/scripts/core-data/index.min.js 37.5 kB +67 B (+0.18%)

compressed-size-action

@swissspidy

Copy link
Copy Markdown
Member

Related:

Centralized resolution invalidation seems less error prone to me. Otherwise it's too easy to make this same mistake in other places as well, which will be confusing for users (stale data in one place, current data in another)

`invalidateResolution` starts a second request for a record while the
first one may still be in flight, and the store kept whichever response
was delivered last. A response whose body was read from the database
before an update could therefore overwrite the updated record, with
nothing left to invalidate afterwards.

Client-side media uploads hit this: the attachment record fetched while
sub-sizes are still being generated carries an empty `media_details.sizes`,
and when that response lands after the post-upload refetch it hides the
Image block's Resolution control for the rest of the session. Existing
invalidation cannot fix it, because the stale response arrives after the
invalidation has already been acted on.

Have `getEntityRecord` drop a response once a newer response for the same
request has been received. Only identical requests are compared, so a
request for a different set of `_fields` never discards another one's
data, and a newer request that fails leaves the older response to be used
as before.

Fixes #81844.
@adamsilverstein
adamsilverstein force-pushed the fix/image-resolution-stale-attachment-record branch from 01a8a71 to c2a8e66 Compare August 20, 2026 18:42
@github-actions github-actions Bot added [Package] Core data /packages/core-data and removed [Package] Block library /packages/block-library labels Aug 20, 2026
…-stale-attachment-record

# Conflicts:
#	packages/core-data/CHANGELOG.md
@adamsilverstein adamsilverstein changed the title Image: Defer attachment resolution until client-side upload finishes Core Data: Discard entity record responses superseded by a newer request Aug 20, 2026
@adamsilverstein

Copy link
Copy Markdown
Member Author

I asked Claude to work through your feedback, here is what came back:

Agreed, and following that thread turned up the real problem.

Centralized invalidation is already in place - mediaUploadOnSuccess invalidates the attachment record after every upload. It cannot fix this one, because the stale response is delivered after that invalidation has already been acted on. The mid-upload GET goes out first, its body is read from the database before finalize writes the sizes, and core-data keeps whichever response arrives last. Invalidating again just loses the same race.

So the fix moved down a layer instead of sideways. getEntityRecord now discards a response once a newer response for the same request has been received, and the Image block change is gone.

It stays conservative on both edges: only identical requests are compared, so a different _fields set never discards another one's data, and a newer request that fails discards nothing - the guard trips only once a newer response has actually been written to the store.

Verified red-then-green at both levels: a new core-data unit test for the ordering itself, and the existing e2e test that reproduces the reported bug by delivering the pre-finalize attachment GET after the finalized record is already cached.

getEntityRecords has the same last-delivered-wins behaviour for lists. Left alone here to keep this change reviewable, and noted as a follow-up in the description.

A batch upload has several attachment records resolving at once. Prove
that a response is only ever superseded by a newer response for the same
request, so one record in flight is never discarded because a different
one was received first.
@adamsilverstein adamsilverstein added the [Type] Bug An existing feature does not function as intended label Aug 20, 2026
@github-actions

Copy link
Copy Markdown

Flaky tests detected in de7cfa9.
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/33114719072
📝 Reported tests:

three users concurrently edit a large post with diverse blocks in /test/e2e/specs/editor/collaboration/collaboration-stress.spec.ts, passed after 1 failed attempt.
TimeoutError: locator.waitFor: Timeout 10000ms exceeded.
Call log:
  - waiting for getByRole('button', { name: 'Dismiss this notice' }).filter({ hasText: 'Draft saved' }) to be visible

    at Editor.saveDraft (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/editor/save-draft.ts:16:4)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/collaboration-stress.spec.ts:407:3

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 [Package] Core data /packages/core-data [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Image block hides Resolution because media REST response contains empty media_details.sizes in WordPress 7.1

2 participants