Skip to content

REST API: Add author filtering to autosave collections. - #7555

Open
adamsilverstein wants to merge 32 commits into
WordPress:trunkfrom
adamsilverstein:ticket/62057
Open

adamsilverstein wants to merge 32 commits into
WordPress:trunkfrom
adamsilverstein:ticket/62057

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Oct 11, 2024 •

Copy link
Copy Markdown
Member

Trac ticket: https://core.trac.wordpress.org/ticket/62057

Gutenberg companion: WordPress/gutenberg#71367

Test in WordPress Playground

Targeting 7.3. This now includes @whyisjake's 62057.5.patch and 62057.6.patch from the ticket.

Description refreshed with Claude after folding in the Trac patches:

What

  • Autosaves are queried directly. get_items() runs one query for the parent's {parent}-autosave-v1 revisions instead of loading every revision with wp_get_post_revisions() and picking the autosaves out. This applies with or without author, so the unscoped request (and the current editor preload) no longer scales with revision count.
  • New author parameter. Takes one or more positive user IDs and adds author__in to that query. Applies to /wp/v2/posts/<id>/autosaves and every other route built on WP_REST_Autosaves_Controller, templates included.
  • Query filters are suppressed, matching the get_children() lookup this replaces, so a global posts_where, posts_join or posts_orderby filter cannot drop or reorder autosaves.
  • One autosave per author. Results are newest first (date, then ID). If an author somehow has more than one stored autosave, only the newest is returned, in both the scoped and unscoped responses.
  • The editor preloads only the current user's autosave. edit-form-blocks.php preloads autosaves?context=edit&author=<current user>, which is what the getAutosave resolver in Core Data: Improve autosave loading gutenberg#71367 requests.

Permissions are unchanged: the unfiltered collection already returns every author's autosave to anyone who can edit the parent, so filtering by author does not expose anything new.

Merge ordering

The preload change has to land in the same release as the package sync that carries WordPress/gutenberg#71367. Before that sync, the editor requests autosaves?context=edit, misses the scoped preload, and makes a network request on every editor load. The REST changes are safe on their own: a client that does not send author gets the same response as before, just without loading every revision.

Testing Instructions

  • Run npm run test:php -- --filter "WP_Test_REST_Autosaves_Controller|Tests_REST_wpRestTemplateAutosavesController".
  • test_get_items_is_isolated_from_query_filters fails for both the scoped and unscoped requests if suppress_filters is set back to false.
  • Manually: as two different editors, make unsaved changes to the same post so each gets an autosave. GET /wp/v2/posts/<id>/autosaves?author=<user_id> returns only that user's autosave, and the request without author returns both.
  • Invalid IDs (author=0, author=-1) return a rest_invalid_param error.

The measurements on the ticket (https://core.trac.wordpress.org/ticket/62057#comment:31) show the unscoped request on a post with 200 ordinary revisions dropping from about 41 MB retained to about 3 MB.

AI Use

Code and description both written with 🤖 Claude Code, building on @whyisjake's patches. I will review and test.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Commit message

REST API: Query autosaves directly and allow filtering by author.

Fix an issue where loading the block editor could run out of memory because the autosaves collection loaded every revision of the post at full content through `wp_get_post_revisions()` just to pick out the autosaves, and the editor preloads that collection on every load.

`WP_REST_Autosaves_Controller::get_items()` now queries the parent's autosaves directly, with query filters suppressed to match the `get_children()` lookup it replaces. A new `author` parameter limits the result to specific users. The block editor only reads the current user's autosave, so `edit-form-blocks.php` now preloads `autosaves?context=edit&author=<current user>`, matching the request made by the `getAutosave` resolver in https://github.com/WordPress/gutenberg/pull/71367.

Requests without `author` return the same autosaves as before, newest first. If an author somehow has more than one stored autosave, only the newest is returned.

Follow-up to [46110].

Props whyisjake, tallulahhh, timothyblynjacobs, westonruter, swissspidy, farhansabirdynasign, mamaduka, ramonopoly.
Fixes #62057.

@github-actions

github-actions Bot commented Oct 11, 2024 •

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.

Core Committers: Use this line as a base for the props when committing in SVN:

Props adamsilverstein, timothyblynjacobs, westonruter.

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


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@adamsilverstein adamsilverstein changed the title Remove autosaves preload, reducing memory footprint Only preload a single autosave May 20, 2025
@adamsilverstein adamsilverstein changed the title Only preload a single autosave remove autosave preload and add per_page support Aug 26, 2025
adamsilverstein and others added 10 commits August 13, 2026 17:29
WordPress stores at most one autosave per author, and the block editor only
ever reads the current user's, so returning every autosave for the post makes
the editor download a collection to discard all but one row of it.

Register an `author` parameter on the autosaves collection, shaped like the
one on the posts controller, and answer it with `wp_get_post_autosave()` for
each requested id. This replaces the `per_page` parameter tried earlier, which
narrowed the revision query before autosaves were filtered out of it and so
could return nothing at all on a post with revision history.

The block editor preload is scoped to match the path the editor requests,
rather than removed: the preload cache is keyed on the path, so an entry the
client never asks for is served to nobody.

See #62057.
…or param.

The new author collection param on the autosaves endpoints changes the
REST schema, so the committed QUnit fixture was stale and every
single-site PHPUnit job failed the version-controlled files check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XGNSFgvkHJVapMFkJo3VU
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DxbbMKz3sa741SjHuE1ZLS
The block editor still requests /autosaves?context=edit without an
author, so an author scoped preload is never consumed: it adds a
request on every load and triggers an unconsumed preload warning.
Keep trunk's preload until the client change lands in core.

See #62057.
Running wp_get_post_autosave() once per requested author made the
query count grow with the size of the author list and returned
duplicates when an ID was repeated. A single author__in query sorted
by date and ID, newest first, matches the ordering of the unscoped
path, and the result is reduced to one autosave per author.

See #62057.
WP_Query ignores an author of 0 and treats negative IDs as
exclusions, so author=0 or author=-1 would return other users'
autosaves instead of narrowing the result.

See #62057.
Adds coverage for author IDs below 1, repeated IDs, ordering parity
with the unscoped collection and the inherited parameter on the
template autosaves route, and corrects a misleading comment on the
extra test users.

See #62057.
@adamsilverstein
adamsilverstein requested review from TimothyBJacobs and westonruter and a balanced review from Copilot October 17, 2026 19:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The ordering and newer-revision tests relied on records created in
sequence, so they mostly exercised the ID tiebreaker. Set dates that
disagree with ID order, and cover an author with two stored autosaves
so the newest-record deduplication runs against real duplicates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aBHgCnrQQ5WWMiPZjtZK1
@adamsilverstein adamsilverstein changed the title remove autosave preload and add per_page support REST API: Add author filtering to autosave collections. Oct 17, 2026
@adamsilverstein adamsilverstein self-assigned this Oct 17, 2026
'post_status' => 'inherit',
'post_parent' => $parent_id,
'name' => $parent_id . '-autosave-v1',
'author__in' => wp_parse_id_list( $request['author'] ),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is wp_parse_id_list() needed?

Is not author already guaranteed to be a list of positive integers per the schema?

'author' => array(
'description' => __( 'Limit result set to autosaves assigned to specific authors.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
'minimum' => 1,
),
'default' => array(),
),

Suggested change
'author__in' => wp_parse_id_list( $request['author'] ),
'author__in' => $request['author'],

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated in 84953d3.

$seen_authors = array();

foreach ( $autosaves as $autosave ) {
if ( isset( $seen_authors[ $autosave->post_author ] ) ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A hash lookup is surely overkill and likely less performant than just a plain array, considering that it is highly unlikely there is ever going to be more than a handful of revisions here, right?

Suggested change
if ( isset( $seen_authors[ $autosave->post_author ] ) ) {
if ( in_array( $autosave->post_author, $seen_authors, true ) ) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

good point

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated in 84953d3.

continue;
}

$seen_authors[ $autosave->post_author ] = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
$seen_authors[ $autosave->post_author ] = true;
$seen_authors[] = $autosave->post_author;

The schema already sanitizes `author` to a list of positive integers, so
running it through wp_parse_id_list() again was redundant. The seen-author
check now uses a plain array, since an author scoped query only ever holds a
handful of autosaves and a hash lookup buys nothing at that size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EzTaxRNEc1fpMMaLJWcNyd

@westonruter westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The following PHPStan errors remain, specifically in the tests:


 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/rest-api/rest-autosaves-controller.php
 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------
  27     Property WP_Test_REST_Autosaves_Controller::$extra_users type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  132    Parameter #1 $user_id of static method WP_UnitTestCase_Base::delete_user() expects int, mixed given.
         🪪  argument.type
  990    Parameter #1 $id of function wp_set_current_user expects int|null, mixed given.
         🪪  argument.type
  991    Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  1000   Method WP_Test_REST_Autosaves_Controller::create_autosaves_for_extra_users() should return array<int> but returns array<int|WP_Error>.
         🪪  return.type
  1010   Method WP_Test_REST_Autosaves_Controller::set_post_date() has no return type specified.
         🪪  missingType.return
  1013   Cannot call method update() on mixed.
         🪪  method.nonObject
  1014   Cannot access property $posts on mixed.
         🪪  property.nonObject
  1037   Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, mixed given.
         🪪  argument.type
  1059   Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, mixed given.
         🪪  argument.type
  1060   Cannot access offset 'id' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  1060   Cannot access offset 0 on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  1060   Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  1061   Cannot access offset 'author' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  1061   Cannot access offset 0 on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  1080   Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, mixed given.
         🪪  argument.type
  1082   Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  1082   Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  1083   Parameter #1 $input_list of function wp_list_pluck expects array, mixed given.
         🪪  argument.type
  1127   Cannot access property $ID on int|WP_Post|false.
         🪪  property.nonObject
  1135   Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, mixed given.
         🪪  argument.type
  1136   Cannot access offset 'id' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  1136   Cannot access offset 0 on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  1136   Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  1189   Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, mixed given.
         🪪  argument.type
  1190   Cannot access offset 'id' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  1190   Cannot access offset 0 on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  1190   Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  1210   Parameter #1 $arr1 of function array_intersect expects an array of values castable to string, array given.
         🪪  argument.type
  1210   Parameter #1 $input_list of function wp_list_pluck expects array, mixed given.
         🪪  argument.type
  1217   Parameter #1 $input_list of function wp_list_pluck expects array, mixed given.
         🪪  argument.type
  1239   Parameter #1 $id of function wp_set_current_user expects int|null, mixed given.
         🪪  argument.type
  1261   Parameter #1 $post_id of method WP_Test_REST_Autosaves_Controller::set_post_date() expects int, int|WP_Error given.
         🪪  argument.type
  1271   Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, mixed given.
         🪪  argument.type
  1272   Cannot access offset 'id' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  1272   Cannot access offset 0 on mixed.
         🪪  offsetAccess.nonOffsetAccessible
 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php
 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------
  921    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, mixed given.
         🪪  argument.type
  922    Cannot access offset 'wp_id' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  922    Cannot access offset 0 on mixed.
         🪪  offsetAccess.nonOffsetAccessible
 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------

 [ERROR] Found 39 errors on changed lines (base: trunk) 

if ( in_array( $autosave->post_author, $seen_authors, true ) ) {
continue;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Micronit: This blank line separates two bits of PHP code which are part of the same “paragraph”.

Suggested change

*
* @return int[] Author id keyed to the autosave id created for them.
*/
private function create_autosaves_for_extra_users() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
private function create_autosaves_for_extra_users() {
private function create_autosaves_for_extra_users(): array {

* @param int $post_id Post ID.
* @param string $date Date in MySQL format.
*/
private function set_post_date( $post_id, $date ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
private function set_post_date( $post_id, $date ) {
private function set_post_date( int $post_id, string $date ): void {

);

$revisions = wp_get_post_revisions( self::$post_id, array( 'check_enabled' => false ) );
$this->set_post_date( reset( $revisions )->ID, '2030-01-01 00:00:00' );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
$this->set_post_date( reset( $revisions )->ID, '2030-01-01 00:00:00' );
$this->set_post_date( array_first( $revisions )->ID, '2030-01-01 00:00:00' );

)
);

$revisions = wp_get_post_revisions( self::$post_id, array( 'check_enabled' => false ) );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
$revisions = wp_get_post_revisions( self::$post_id, array( 'check_enabled' => false ) );
$revisions = wp_get_post_revisions( self::$post_id, array( 'check_enabled' => false ) );
$this->assertCount( 1, $revisions );

Comment on lines +1119 to +1124
wp_update_post(
array(
'ID' => self::$post_id,
'post_content' => 'A revision made after every autosave.',
)
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might want to assert that this returns true.

Comment on lines +1162 to +1164
* @return array[]
*/
public function data_get_items_rejects_invalid_author() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* @return array[]
*/
public function data_get_items_rejects_invalid_author() {
* @return array<non-falsy-string, array<mixed>>
*/
public function data_get_items_rejects_invalid_author(): array {

Comment on lines +1278 to +1281
* @return array[]
*/
public function data_get_items_with_several_autosaves_for_one_author() {
return array(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* @return array[]
*/
public function data_get_items_with_several_autosaves_for_one_author() {
return array(
* @return array<non-falsy-string, array{ non-falsy-string, non-falsy-string, non-negative-int }>
*/
public function data_get_items_with_several_autosaves_for_one_author(): array {
return array(

* @param string $second_date Date of the autosave created second.
* @param int $expected Index of the autosave expected to be returned.
*/
public function test_get_items_with_several_autosaves_for_one_author( $first_date, $second_date, $expected ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
public function test_get_items_with_several_autosaves_for_one_author( $first_date, $second_date, $expected ) {
public function test_get_items_with_several_autosaves_for_one_author( string $first_date, string $second_date, int $expected ) {

Comment on lines +1232 to +1234
* @param string $first_date Date of the autosave created first.
* @param string $second_date Date of the autosave created second.
* @param int $expected Index of the autosave expected to be returned.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* @param string $first_date Date of the autosave created first.
* @param string $second_date Date of the autosave created second.
* @param int $expected Index of the autosave expected to be returned.
* @param non-falsy-string $first_date Date of the autosave created first.
* @param non-falsy-string $second_date Date of the autosave created second.
* @param non-negative-int $expected Index of the autosave expected to be returned.

adamsilverstein and others added 6 commits November 7, 2026 20:08
Add native and PHPDoc types, narrow REST response data with assertions
before reading it, and assert that autosave and post updates succeed so
a failure surfaces at its source rather than as a later type error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vwYjTSTv92Mmx6whS2YN5
The block editor requests the current user's autosave with the author
parameter, and the preload cache is keyed on the full path, so the
preloaded path has to carry the same parameter to be used.
Requests without the author parameter still loaded every revision of the
post to pick the autosaves out, including the editor preload, so memory
kept scaling with revision count. Both paths now run one autosave query.

Suppress query filters on it, matching the get_children() lookup it
replaces, so a global posts_where filter cannot drop or reorder autosaves.

From 62057.6.patch by whyisjake.
@adamsilverstein

Copy link
Copy Markdown
Member Author

Thanks @westonruter, these are fixed in b6bcc55.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The three new data providers should be declared public static to match existing providers, since none uses instance state.

1 open issue

Review effort: Balanced

*
* @return array<non-falsy-string, array<mixed>>
*/
public function data_get_items_rejects_invalid_author(): array {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants