Make WordPress Core


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