Make WordPress Core


Ignore:
Timestamp:
03/02/2025 10:05:08 PM (18 months ago)
Author:
TimothyBlynJacobs
Message:

REST API: Improve performance for HEAD requests.

By default, the REST API responds to HEAD rqeuests by calling the GET handler and omitting the body from the response. While convenient, this ends up performing needless work that slows down the API response time.

This commit adjusts the Core controllers to specifically handle HEAD requests by not preparing the response body.

Fixes #56481.
Props antonvlasenko, janusdev, ironprogrammer, swissspidy, spacedmonkey, mukesh27, mamaduka, timothyblynjacobs.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/rest-api/rest-categories-controller.php

    r56746 r59899  
    577577        }
    578578
    579         public function test_get_terms_invalid_parent_arg() {
    580                 $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     579        /**
     580         * @dataProvider data_readable_http_methods
     581         * @ticket 56481
     582         *
     583         * @param string $method HTTP method to use.
     584         */
     585        public function test_get_terms_invalid_parent_arg( $method ) {
     586                $request = new WP_REST_Request( $method, '/wp/v2/categories' );
    581587                $request->set_param( 'parent', 'invalid-parent' );
    582588                $response = rest_get_server()->dispatch( $request );
     
    610616        }
    611617
    612         public function test_get_terms_pagination_headers() {
     618        /**
     619         * @dataProvider data_readable_http_methods
     620         * @ticket 56481
     621         *
     622         * @param string $method HTTP method to use.
     623         */
     624        public function test_get_terms_pagination_headers( $method ) {
    613625                $total_categories = self::$total_categories;
    614626                $total_pages      = (int) ceil( $total_categories / 10 );
    615627
    616628                // Start of the index + Uncategorized default term.
    617                 $request  = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     629                $request  = new WP_REST_Request( $method, '/wp/v2/categories' );
    618630                $response = rest_get_server()->dispatch( $request );
    619631                $headers  = $response->get_headers();
    620632                $this->assertSame( $total_categories, $headers['X-WP-Total'] );
    621633                $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] );
    622                 $this->assertCount( 10, $response->get_data() );
     634                if ( 'HEAD' !== $method ) {
     635                        $this->assertCount( 10, $response->get_data() );
     636                }
    623637                $next_link = add_query_arg(
    624638                        array(
     
    663677                $this->assertSame( $total_categories, $headers['X-WP-Total'] );
    664678                $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] );
    665                 $this->assertCount( 1, $response->get_data() );
     679                if ( 'HEAD' !== $method ) {
     680                        $this->assertCount( 1, $response->get_data() );
     681                }
    666682                $prev_link = add_query_arg(
    667683                        array(
     
    680696                $this->assertSame( $total_categories, $headers['X-WP-Total'] );
    681697                $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] );
    682                 $this->assertCount( 0, $response->get_data() );
     698                if ( 'HEAD' !== $method ) {
     699                        $this->assertCount( 0, $response->get_data() );
     700                }
    683701                $prev_link = add_query_arg(
    684702                        array(
     
    12371255                $this->check_taxonomy_term( $category, $data, $response->get_links() );
    12381256        }
     1257
     1258        /**
     1259         * @dataProvider data_readable_http_methods
     1260         * @ticket 56481
     1261         *
     1262         * @param string $method HTTP method to use.
     1263         */
     1264        public function test_get_items_only_fetches_ids_for_head_requests( $method ) {
     1265                $is_head_request = 'HEAD' === $method;
     1266                $request         = new WP_REST_Request( $method, '/wp/v2/categories' );
     1267
     1268                $filter = new MockAction();
     1269
     1270                add_filter( 'terms_pre_query', array( $filter, 'filter' ), 10, 2 );
     1271
     1272                $response = rest_get_server()->dispatch( $request );
     1273
     1274                $this->assertSame( 200, $response->get_status() );
     1275                if ( $is_head_request ) {
     1276                        $this->assertEmpty( $response->get_data() );
     1277                } else {
     1278                        $this->assertNotEmpty( $response->get_data() );
     1279                }
     1280
     1281                $args = $filter->get_args();
     1282                $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' );
     1283                $this->assertInstanceOf( WP_Term_Query::class, $args[0][1], 'Query parameters were not captured.' );
     1284
     1285                /** @var WP_Term_Query $query */
     1286                $query = $args[0][1];
     1287
     1288                if ( $is_head_request ) {
     1289                        $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' );
     1290                        $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only term IDs.' );
     1291                        $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' );
     1292                        $this->assertFalse( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be false for HEAD requests.' );
     1293                } else {
     1294                        $this->assertTrue(
     1295                                ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'],
     1296                                'The fields parameter should not be forced to "ids" for non-HEAD requests.'
     1297                        );
     1298                        $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' );
     1299                        $this->assertTrue( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be true for HEAD requests.' );
     1300                }
     1301
     1302                if ( ! $is_head_request ) {
     1303                        return;
     1304                }
     1305
     1306                global $wpdb;
     1307                $terms_table = preg_quote( $wpdb->terms, '/' );
     1308
     1309                $pattern = '/SELECT\s+t\.term_id.+FROM\s+' . $terms_table . '\s+AS\s+t\s+INNER\s+JOIN/is';
     1310
     1311                // Assert that the SQL query only fetches the term_id column.
     1312                $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' );
     1313        }
     1314
     1315        /**
     1316         * Data provider intended to provide HTTP method names for testing GET and HEAD requests.
     1317         *
     1318         * @return array
     1319         */
     1320        public static function data_readable_http_methods() {
     1321                return array(
     1322                        'GET request'  => array( 'GET' ),
     1323                        'HEAD request' => array( 'HEAD' ),
     1324                );
     1325        }
     1326
     1327        /**
     1328         * @dataProvider data_readable_http_methods
     1329         * @ticket 56481
     1330         *
     1331         * @param string $method The HTTP method to use.
     1332         */
     1333        public function test_get_item_should_allow_adding_headers_via_filter( $method ) {
     1334                $category_id = self::factory()->category->create();
     1335
     1336                $request = new WP_REST_Request( $method, sprintf( '/wp/v2/categories/%d', $category_id ) );
     1337
     1338                $hook_name = 'rest_prepare_category';
     1339
     1340                $filter   = new MockAction();
     1341                $callback = array( $filter, 'filter' );
     1342                add_filter( $hook_name, $callback );
     1343                $header_filter = new class() {
     1344                        public static function add_custom_header( $response ) {
     1345                                $response->header( 'X-Test-Header', 'Test' );
     1346
     1347                                return $response;
     1348                        }
     1349                };
     1350                add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) );
     1351                $response = rest_get_server()->dispatch( $request );
     1352                remove_filter( $hook_name, $callback );
     1353                remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) );
     1354
     1355                $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
     1356                $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' );
     1357                $headers = $response->get_headers();
     1358                $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' );
     1359                $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' );
     1360                if ( 'HEAD' !== $method ) {
     1361                        return null;
     1362                }
     1363                $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' );
     1364        }
    12391365}
Note: See TracChangeset for help on using the changeset viewer.