Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0065b16
Add preload paths filter introduced in https://github.com/WordPress/g…
westonruter May 10, 2026
1b0d88d
Preload REST API requests used on the Connectors screen
westonruter May 10, 2026
7f32ee4
Supply improved type for WP_HTTP_Response::$headers
westonruter May 10, 2026
9b38307
Harden rest_preload_api_request() and address PHPStan errors
westonruter May 10, 2026
d4b1ec0
Allow preloading REST API responses with non-OK statuses
westonruter May 10, 2026
0352706
Update phpdoc for _wp_connectors_preload_paths()
westonruter May 11, 2026
35123b5
Revert "Add preload paths filter introduced in https://github.com/Wor…
westonruter May 11, 2026
416c453
Fix duplicated non-existing post ID
westonruter May 12, 2026
ad4c7e4
Fix variable reference in assertion message
westonruter May 12, 2026
0fea7da
Merge branch 'trunk' into trac-65215
westonruter Jun 14, 2026
e6131bc
Update since tag
westonruter Jun 15, 2026
6d5a4c0
Merge branch 'trunk' into trac-65215
westonruter Jun 15, 2026
01e3dc0
Fix PHPStan rule level 10 issues in tests
westonruter Jun 16, 2026
8cadac3
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Jun 16, 2026
849ad25
Merge branch 'trunk' into trac-65215
t-hamano Jul 28, 2026
9185ca5
Fix typo
t-hamano Aug 2, 2026
b202b62
Merge branch 'trunk' into trac-65215
t-hamano Aug 2, 2026
e9f0099
Fix typo
t-hamano Aug 2, 2026
fc55a4a
Merge branch 'trunk' into trac-65215
peterwilsoncc Sep 29, 2026
6236869
Remove obsolete PHPStan baseline entry for rest_preload_api_request()
t-hamano Sep 29, 2026
71701fd
Rename Connectors preload paths hook to match the Gutenberg wp-build …
t-hamano Sep 29, 2026
030f827
Merge branch 'trunk' into trac-65215
t-hamano Oct 2, 2026
2d55126
Update PHPDoc for Connectors preloading and rest_preload_api_request()
t-hamano Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/wp-admin/options-connectors.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,45 @@
// Set parent file for menu highlighting.
$parent_file = 'options-general.php';

/**
* Preloads the REST API responses the Connectors UI fetches on mount.
*
* Without this, the page does a network round-trip for site settings,
* plugin capability discovery, and each connector's plugin record after
* the JS hydrates, which noticeably delays first paint.
*
* @since 7.2.0
* @access private
*
* @param array<string|array{ 0: string, 1?: 'GET'|'OPTIONS', 2?: int<100, 599>|int<100, 599>[] }> $preload_paths Paths already queued for preloading.
* @return array<string|array{ 0: string, 1?: 'GET'|'OPTIONS', 2?: int<100, 599>|int<100, 599>[] }> Paths with the Connectors-specific requests appended.
*/
function _wp_connectors_preload_paths( array $preload_paths ): array {
// getEntityRecord( 'root', 'site' ) in use-connector-plugin.ts / ai-plugin-callout.tsx.
$preload_paths[] = '/wp/v2/settings';

// canUser( 'create', { kind: 'root', name: 'plugin' } ) in stage.tsx.
$preload_paths[] = array( '/wp/v2/plugins', 'OPTIONS' );

// AiPluginCallout in routes/connectors-home/ai-plugin-callout.tsx queries this
// hardcoded ID to check whether the WP AI plugin is installed/active.
$preload_paths[] = array( '/wp/v2/plugins/ai/ai?context=edit', 'GET', array( 200, 404 ) );

// getEntityRecord( 'root', 'plugin', <basename> ) per connector in use-connector-plugin.ts.
foreach ( wp_get_connectors() as $connector_data ) {
if ( empty( $connector_data['plugin']['file'] ) ) {
continue;
}
// core-data's plugin entity uses the basename with `.php` stripped
// as the record key (see routes/connectors-home/use-connector-plugin.ts).
$basename = preg_replace( '/\.php$/', '', plugin_basename( $connector_data['plugin']['file'] ) );
$preload_paths[] = array( '/wp/v2/plugins/' . $basename . '?context=edit', 'GET', array( 200, 404 ) );
}

return $preload_paths;
}
add_filter( 'wp_options_connectors_wp_admin_preload_paths', '_wp_connectors_preload_paths' );

Comment on lines +70 to +73
require_once ABSPATH . 'wp-admin/admin-header.php';

// Render the Connectors page.
Expand Down
2 changes: 1 addition & 1 deletion src/wp-includes/class-wp-http-response.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class WP_HTTP_Response {
* Response headers.
*
* @since 4.4.0
* @var array
* @var array<string, string>
*/
public $headers;

Expand Down
51 changes: 39 additions & 12 deletions src/wp-includes/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -2981,10 +2981,18 @@ function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
* Expected to be called in the context of `array_reduce`.
*
* @since 5.0.0
* @since 7.2.0 The `$path` array may include a third element for the allowed response statuses.
*
* @param array $memo Reduce accumulator.
* @param string $path REST API path to preload.
* @param array $memo Reduce accumulator.
* @param string|array $path REST API path to preload, or an array containing the path,
* the HTTP method ('GET' or 'OPTIONS'), and the allowed response
* status code(s). Only responses with an allowed status are
* preloaded. Default allowed status is 200.
* @return array Modified reduce accumulator.
*
* @phpstan-param array<string, array{ body: array<mixed>, headers: array<string, string> } | array<string, array{ body: array<mixed>, headers: array<string, string> }> > $memo
* @phpstan-param string|array{ 0: string, 1?: 'GET'|'OPTIONS', 2?: int<100, 599>|int<100, 599>[] } $path
* @phpstan-return array<string, array{ body: array<mixed>, headers: array<string, string> } | array<string, array{ body: array<mixed>, headers: array<string, string> }> >
*/
function rest_preload_api_request( $memo, $path ) {

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 change looks good, but getting it very close to the end of the release seems a little riskier it may be preferable to merge right after or for now just preload non 404 requests?

/*
Expand All @@ -2999,14 +3007,25 @@ function rest_preload_api_request( $memo, $path ) {
return $memo;
}

$method = 'GET';
if ( is_array( $path ) && 2 === count( $path ) ) {
$method = end( $path );
$path = reset( $path );

$method = 'GET';
$allowed_statuses = array( 200 );
if ( is_array( $path ) ) {
$path_array = $path;
$path = array_shift( $path_array );
if ( ! is_string( $path ) ) {
return $memo;
}
$method = array_shift( $path_array );
if ( ! in_array( $method, array( 'GET', 'OPTIONS' ), true ) ) {
$method = 'GET';
}
$statuses = array_shift( $path_array );
if ( $statuses ) {
$statuses = array_filter( (array) $statuses, 'is_int' );
if ( count( $statuses ) > 0 ) {
$allowed_statuses = $statuses;
}
}
}

// Remove trailing slashes at the end of the REST API path (query part).
Expand All @@ -3016,11 +3035,11 @@ function rest_preload_api_request( $memo, $path ) {
}

$path_parts = parse_url( $path );
if ( false === $path_parts ) {
if ( false === $path_parts || ! isset( $path_parts['path'] ) ) {
return $memo;
}

if ( isset( $path_parts['path'] ) && '/' !== $path_parts['path'] ) {
if ( '/' !== $path_parts['path'] ) {
// Remove trailing slashes from the "path" part of the REST API path.
$path_parts['path'] = untrailingslashit( $path_parts['path'] );
$path = str_contains( $path, '?' ) ?
Expand All @@ -3035,12 +3054,20 @@ function rest_preload_api_request( $memo, $path ) {
}

$response = rest_do_request( $request );
if ( 200 === $response->status ) {
if ( in_array( $response->status, $allowed_statuses, true ) ) {
$server = rest_get_server();
/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
$response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $server, $request );
$embed = $request->has_param( '_embed' ) ? rest_parse_embed_param( $request['_embed'] ) : false;
$data = (array) $server->response_to_data( $response, $embed );
if ( ! $response instanceof WP_REST_Response ) {
return $memo;
}

if ( $request->has_param( '_embed' ) && ( is_array( $request['_embed'] ) || is_string( $request['_embed'] ) ) ) {
$embed = rest_parse_embed_param( $request['_embed'] );
} else {
$embed = false;
}
$data = (array) $server->response_to_data( $response, $embed );
Comment thread
westonruter marked this conversation as resolved.

if ( 'OPTIONS' === $method ) {
$memo[ $method ][ $path ] = array(
Expand Down
5 changes: 0 additions & 5 deletions tests/phpstan/baselines/argument.type.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1188,11 +1188,6 @@ parameters:
identifier: argument.type
count: 1
path: ../../../src/wp-includes/post.php
-
message: '#^Parameter \#1 \$response of method WP_REST_Server\:\:response_to_data\(\) expects WP_REST_Response, WP_HTTP_Response given\.$#'
identifier: argument.type
count: 1
path: ../../../src/wp-includes/rest-api.php
-
message: '#^Parameter \#1 \$haystack of function str_starts_with expects string, int given\.$#'
identifier: argument.type
Expand Down
29 changes: 27 additions & 2 deletions tests/phpunit/tests/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -1093,9 +1093,22 @@ public function test_register_rest_route_without_server() {
$this->assertSame( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true ) );
}

public function test_rest_preload_api_request_with_method() {
/**
* @ticket 65215
*/
public function test_rest_preload_api_request_with_method_and_allowed_statuses(): void {
$existing_post_id = self::factory()->post->create();
$this->assertIsInt( $existing_post_id );
$missing_post1_id = 10001;
$missing_post2_id = 10002;
$this->assertNull( get_post( $missing_post1_id ), "Expected post with ID $missing_post1_id to not exist." );
$this->assertNull( get_post( $missing_post2_id ), "Expected post with ID $missing_post2_id to not exist." );

$preload_paths = array(
'/wp/v2/types',
array( "/wp/v2/posts/$existing_post_id", 'GET', array( 200 ) ),
array( "/wp/v2/posts/$missing_post1_id", 'GET', array( 200, 404 ) ),
array( "/wp/v2/posts/$missing_post2_id", 'GET' ),
array( '/wp/v2/media', 'OPTIONS' ),
);

Expand All @@ -1105,8 +1118,20 @@ public function test_rest_preload_api_request_with_method() {
array()
);

$this->assertSame( array_keys( $preload_data ), array( '/wp/v2/types', 'OPTIONS' ) );
$this->assertSame( array_keys( $preload_data ), array( '/wp/v2/types', "/wp/v2/posts/$existing_post_id", "/wp/v2/posts/$missing_post1_id", 'OPTIONS' ) );
$this->assertArrayHasKey( '/wp/v2/media', $preload_data['OPTIONS'] );

$existing_post_response_data = $preload_data[ "/wp/v2/posts/$existing_post_id" ];
$this->assertTrue( isset( $existing_post_response_data['body']['id'] ), 'Expected body.id to be exist.' );
$this->assertSame( $existing_post_id, $existing_post_response_data['body']['id'] );

$missing_post_response_data = $preload_data[ "/wp/v2/posts/$missing_post1_id" ];
$this->assertTrue( isset( $missing_post_response_data['body']['code'] ), 'Expected body.code to exist.' );
$this->assertTrue( isset( $missing_post_response_data['body']['data'] ), 'Expected body.data to exist.' );
$this->assertIsArray( $missing_post_response_data['body']['data'], 'Expected body.data to be an array.' );
$this->assertTrue( isset( $missing_post_response_data['body']['data']['status'] ), 'Expected body.data.status to exist.' );
$this->assertSame( 'rest_post_invalid_id', $missing_post_response_data['body']['code'] );
$this->assertSame( 404, $missing_post_response_data['body']['data']['status'] );
}

/**
Expand Down
Loading