Make WordPress Core


Ignore:
Timestamp:
03/02/2025 10:05:08 PM (20 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-users-controller.php

    r59892 r59899  
    235235        }
    236236
    237         public function test_get_items_with_edit_context() {
    238                 wp_set_current_user( self::$user );
    239 
    240                 $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     237        /**
     238         * @dataProvider data_readable_http_methods
     239         * @ticket 56481
     240         *
     241         * @param string $method HTTP method to use.
     242         */
     243        public function test_get_items_with_edit_context( $method ) {
     244                wp_set_current_user( self::$user );
     245
     246                $request = new WP_REST_Request( $method, '/wp/v2/users' );
    241247                $request->set_param( 'context', 'edit' );
    242248                $response = rest_get_server()->dispatch( $request );
    243249
    244                 $this->assertSame( 200, $response->get_status() );
     250                $this->assertSame(
     251                        200,
     252                        $response->get_status(),
     253                        sprintf( 'Expected HTTP status code 200 but got %s.', $response->get_status() )
     254                );
     255
     256                if ( 'HEAD' === $method ) {
     257                        $this->assertNull( $response->get_data(), 'Expected null response data for HEAD request, but received non-null data.' );
     258                        return null;
     259                }
    245260
    246261                $all_data = $response->get_data();
    … …  
    250265        }
    251266
    252         public function test_get_items_with_edit_context_without_permission() {
     267        /**
     268         * Data provider intended to provide HTTP method names for testing GET and HEAD requests.
     269         *
     270         * @return array
     271         */
     272        public static function data_readable_http_methods() {
     273                return array(
     274                        'GET request'  => array( 'GET' ),
     275                        'HEAD request' => array( 'HEAD' ),
     276                );
     277        }
     278
     279        /**
     280         * @dataProvider data_readable_http_methods
     281         * @ticket 56481
     282         *
     283         * @param string $method HTTP method to use.
     284         */
     285        public function test_get_items_with_edit_context_without_permission( $method ) {
    253286                // Test with a user not logged in.
    254                 $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     287                $request = new WP_REST_Request( $method, '/wp/v2/users' );
    255288                $request->set_param( 'context', 'edit' );
    256289                $response = rest_get_server()->dispatch( $request );
    … …  
    262295                wp_set_current_user( self::$editor );
    263296
    264                 $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     297                $request = new WP_REST_Request( $method, '/wp/v2/users' );
    265298                $request->set_param( 'context', 'edit' );
    266299                $response = rest_get_server()->dispatch( $request );
    … …  
    320353        }
    321354
    322         public function test_get_items_pagination_headers() {
     355        /**
     356         * @dataProvider data_readable_http_methods
     357         * @ticket 56481
     358         *
     359         * @param string $method HTTP method to use.
     360         */
     361        public function test_get_items_pagination_headers( $method ) {
    323362                $total_users = self::$total_users;
    324363                $total_pages = (int) ceil( $total_users / 10 );
    … …  
    327366
    328367                // Start of the index.
    329                 $request  = new WP_REST_Request( 'GET', '/wp/v2/users' );
     368                $request  = new WP_REST_Request( $method, '/wp/v2/users' );
    330369                $response = rest_get_server()->dispatch( $request );
    331370                $headers  = $response->get_headers();
    … …  
    345384                ++$total_users;
    346385                ++$total_pages;
    347                 $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     386                $request = new WP_REST_Request( $method, '/wp/v2/users' );
    348387                $request->set_param( 'page', 3 );
    349388                $response = rest_get_server()->dispatch( $request );
    … …  
    367406
    368407                // Last page.
    369                 $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     408                $request = new WP_REST_Request( $method, '/wp/v2/users' );
    370409                $request->set_param( 'page', $total_pages );
    371410                $response = rest_get_server()->dispatch( $request );
    … …  
    383422
    384423                // Out of bounds.
    385                 $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     424                $request = new WP_REST_Request( $method, '/wp/v2/users' );
    386425                $request->set_param( 'page', 100 );
    387426                $response = rest_get_server()->dispatch( $request );
    … …  
    412451        }
    413452
    414         public function test_get_items_page() {
    415                 wp_set_current_user( self::$user );
    416 
    417                 $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     453        /**
     454         * @dataProvider data_readable_http_methods
     455         * @ticket 56481
     456         *
     457         * @param string $method HTTP method to use.
     458         */
     459        public function test_get_items_page( $method ) {
     460                wp_set_current_user( self::$user );
     461
     462                $request = new WP_REST_Request( $method, '/wp/v2/users' );
    418463                $request->set_param( 'per_page', 5 );
    419464                $request->set_param( 'page', 2 );
    420465                $response = rest_get_server()->dispatch( $request );
    421                 $this->assertCount( 5, $response->get_data() );
     466
     467                if ( 'HEAD' !== $method ) {
     468                        $this->assertCount( 5, $response->get_data() );
     469                }
     470
    422471                $prev_link = add_query_arg(
    423472                        array(
    … …  
    12661315        }
    12671316
    1268         public function test_get_current_user() {
    1269                 wp_set_current_user( self::$user );
    1270 
    1271                 $request  = new WP_REST_Request( 'GET', '/wp/v2/users/me' );
     1317        /**
     1318         * @dataProvider data_readable_http_methods
     1319         * @ticket 56481
     1320         *
     1321         * @param string $method HTTP method to use.
     1322         */
     1323        public function test_get_current_user( $method ) {
     1324                wp_set_current_user( self::$user );
     1325
     1326                $request  = new WP_REST_Request( $method, '/wp/v2/users/me' );
    12721327                $response = rest_get_server()->dispatch( $request );
    12731328                $this->assertSame( 200, $response->get_status() );
    1274                 $this->check_get_user_response( $response, 'view' );
    1275 
    12761329                $headers = $response->get_headers();
    12771330                $this->assertArrayNotHasKey( 'Location', $headers );
    12781331
     1332                if ( 'HEAD' === $method ) {
     1333                        // HEAD responses only contain headers. Bail.
     1334                        return null;
     1335                }
     1336                $this->check_get_user_response( $response, 'view' );
    12791337                $links = $response->get_links();
    12801338                $this->assertSame( rest_url( 'wp/v2/users/' . self::$user ), $links['self'][0]['href'] );
    … …  
    31593217        }
    31603218
     3219        /**
     3220         * @dataProvider data_readable_http_methods
     3221         * @ticket 56481
     3222         *
     3223         * @param string $method The HTTP method to use.
     3224         */
     3225        public function test_get_item_should_allow_adding_headers_via_filter( $method ) {
     3226                wp_set_current_user( self::$user );
     3227                $request = new WP_REST_Request( $method, sprintf( '/wp/v2/users/%d', self::$user ) );
     3228
     3229                $hook_name = 'rest_prepare_user';
     3230
     3231                $filter   = new MockAction();
     3232                $callback = array( $filter, 'filter' );
     3233                add_filter( $hook_name, $callback );
     3234                $header_filter = new class() {
     3235                        public static function add_custom_header( $response ) {
     3236                                $response->header( 'X-Test-Header', 'Test' );
     3237
     3238                                return $response;
     3239                        }
     3240                };
     3241                add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) );
     3242                $response = rest_get_server()->dispatch( $request );
     3243                remove_filter( $hook_name, $callback );
     3244                remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) );
     3245
     3246                $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
     3247                $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' );
     3248                $headers = $response->get_headers();
     3249                $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' );
     3250                $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' );
     3251                if ( 'HEAD' !== $method ) {
     3252                        return null;
     3253                }
     3254                $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' );
     3255        }
     3256
     3257        /**
     3258         * @dataProvider data_readable_http_methods
     3259         * @ticket 56481
     3260         *
     3261         * @param string $method HTTP method to use.
     3262         */
     3263        public function test_get_items_only_fetches_ids_for_head_requests( $method ) {
     3264                $is_head_request = 'HEAD' === $method;
     3265                $request         = new WP_REST_Request( $method, '/wp/v2/users' );
     3266
     3267                $filter = new MockAction();
     3268
     3269                add_filter( 'pre_user_query', array( $filter, 'filter' ), 10, 2 );
     3270
     3271                $response = rest_get_server()->dispatch( $request );
     3272
     3273                $this->assertSame( 200, $response->get_status() );
     3274                if ( $is_head_request ) {
     3275                        $this->assertNull( $response->get_data() );
     3276                } else {
     3277                        $this->assertNotEmpty( $response->get_data() );
     3278                }
     3279
     3280                $args = $filter->get_args();
     3281                $this->assertTrue( isset( $args[0][0] ), 'Query parameters were not captured.' );
     3282                $this->assertInstanceOf( WP_User_Query::class, $args[0][0], 'Query parameters were not captured.' );
     3283
     3284                /** @var WP_User $query */
     3285                $query = $args[0][0];
     3286
     3287                if ( $is_head_request ) {
     3288                        $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' );
     3289                        $this->assertSame( 'id', $query->query_vars['fields'], 'The query must fetch only user IDs.' );
     3290                } else {
     3291                        $this->assertTrue(
     3292                                ! array_key_exists( 'fields', $query->query_vars ) || 'id' !== $query->query_vars['fields'],
     3293                                'The fields parameter should not be forced to "id" for non-HEAD requests.'
     3294                        );
     3295                }
     3296
     3297                if ( ! $is_head_request ) {
     3298                        return;
     3299                }
     3300
     3301                global $wpdb;
     3302                $users_table = preg_quote( $wpdb->users, '/' );
     3303                $pattern     = '/SELECT SQL_CALC_FOUND_ROWS wptests_users.ID\n\s+FROM\s+' . $users_table . '/is';
     3304
     3305                // Assert that the SQL query only fetches the id column.
     3306                $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' );
     3307        }
     3308
    31613309        protected function check_user_data( $user, $data, $context, $links ) {
    31623310                $this->assertSame( $user->ID, $data['id'] );
Note: See TracChangeset for help on using the changeset viewer.