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-comments-controller.php

    r57176 r59899  
    433433    }
    434434
    435     public function test_get_items_no_permission_for_no_post() {
     435    /**
     436     * @dataProvider data_readable_http_methods
     437     * @ticket 56481
     438     *
     439     * @param string $method HTTP method to use.
     440     */
     441    public function test_get_items_no_permission_for_no_post( $method ) {
    436442        wp_set_current_user( 0 );
    437443
    438         $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     444        $request = new WP_REST_Request( $method, '/wp/v2/comments' );
    439445        $request->set_param( 'post', 0 );
    440446        $response = rest_get_server()->dispatch( $request );
     
    442448    }
    443449
    444     public function test_get_items_edit_context() {
    445         wp_set_current_user( self::$admin_id );
    446 
    447         $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     450    /**
     451     * Data provider intended to provide HTTP method names for testing GET and HEAD requests.
     452     *
     453     * @return array
     454     */
     455    public static function data_readable_http_methods() {
     456        return array(
     457            'GET request'  => array( 'GET' ),
     458            'HEAD request' => array( 'HEAD' ),
     459        );
     460    }
     461
     462    /**
     463     * @dataProvider data_readable_http_methods
     464     * @ticket 56481
     465     *
     466     * @param string $method HTTP method to use.
     467     */
     468    public function test_get_items_edit_context( $method ) {
     469        wp_set_current_user( self::$admin_id );
     470
     471        $request = new WP_REST_Request( $method, '/wp/v2/comments' );
    448472        $request->set_param( 'context', 'edit' );
    449473        $response = rest_get_server()->dispatch( $request );
     
    594618    }
    595619
    596     public function test_get_items_private_post_no_permissions() {
     620    /**
     621     * @dataProvider data_readable_http_methods
     622     * @ticket 56481
     623     *
     624     * @param string $method HTTP method to use.
     625     */
     626    public function test_get_items_private_post_no_permissions( $method ) {
    597627        wp_set_current_user( 0 );
    598628
    599629        $post_id = self::factory()->post->create( array( 'post_status' => 'private' ) );
    600630
    601         $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     631        $request = new WP_REST_Request( $method, '/wp/v2/comments' );
    602632        $request->set_param( 'post', $post_id );
    603633        $response = rest_get_server()->dispatch( $request );
     
    802832    }
    803833
    804     public function test_get_comments_pagination_headers() {
     834    /**
     835     * @dataProvider data_readable_http_methods
     836     * @ticket 56481
     837     *
     838     * @param string $method HTTP method to use.
     839     */
     840    public function test_get_comments_pagination_headers( $method ) {
    805841        $total_comments = self::$total_comments;
    806842        $total_pages    = (int) ceil( $total_comments / 10 );
     
    809845
    810846        // Start of the index.
    811         $request  = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     847        $request  = new WP_REST_Request( $method, '/wp/v2/comments' );
    812848        $response = rest_get_server()->dispatch( $request );
    813849        $headers  = $response->get_headers();
     
    885921    }
    886922
    887     public function test_get_comments_invalid_date() {
    888         $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     923    /**
     924     * @dataProvider data_readable_http_methods
     925     * @ticket 56481
     926     *
     927     * @param string $method HTTP method to use.
     928     */
     929    public function test_get_comments_invalid_date( $method ) {
     930        $request = new WP_REST_Request( $method, '/wp/v2/comments' );
    889931        $request->set_param( 'after', 'foo' );
    890932        $request->set_param( 'before', 'bar' );
     
    9981040    }
    9991041
    1000     public function test_get_comment_invalid_id() {
    1001         $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     1042    /**
     1043     * @dataProvider data_readable_http_methods
     1044     * @ticket 56481
     1045     *
     1046     * @param string $method HTTP method to use.
     1047     */
     1048    public function test_get_comment_invalid_id( $method ) {
     1049        $request = new WP_REST_Request( $method, '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
    10021050
    10031051        $response = rest_get_server()->dispatch( $request );
     
    10051053    }
    10061054
    1007     public function test_get_comment_invalid_context() {
     1055    /**
     1056     * @dataProvider data_readable_http_methods
     1057     * @ticket 56481
     1058     *
     1059     * @param string $method HTTP method to use.
     1060     */
     1061    public function test_get_comment_invalid_context( $method ) {
    10081062        wp_set_current_user( 0 );
    10091063
    1010         $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', self::$approved_id ) );
     1064        $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%s', self::$approved_id ) );
    10111065        $request->set_param( 'context', 'edit' );
    10121066        $response = rest_get_server()->dispatch( $request );
     
    10141068    }
    10151069
    1016     public function test_get_comment_invalid_post_id() {
     1070    /**
     1071     * @dataProvider data_readable_http_methods
     1072     * @ticket 56481
     1073     *
     1074     * @param string $method HTTP method to use.
     1075     */
     1076    public function test_get_comment_invalid_post_id( $method ) {
    10171077        wp_set_current_user( 0 );
    10181078
     
    10241084        );
    10251085
    1026         $request  = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id );
     1086        $request  = new WP_REST_Request( $method, '/wp/v2/comments/' . $comment_id );
    10271087        $response = rest_get_server()->dispatch( $request );
    10281088        $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
    10291089    }
    10301090
    1031     public function test_get_comment_invalid_post_id_as_admin() {
     1091    /**
     1092     * @dataProvider data_readable_http_methods
     1093     * @ticket 56481
     1094     *
     1095     * @param string $method HTTP method to use.
     1096     */
     1097    public function test_get_comment_invalid_post_id_as_admin( $method ) {
    10321098        wp_set_current_user( self::$admin_id );
    10331099
     
    10391105        );
    10401106
    1041         $request  = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id );
     1107        $request  = new WP_REST_Request( $method, '/wp/v2/comments/' . $comment_id );
    10421108        $response = rest_get_server()->dispatch( $request );
    10431109        $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
    10441110    }
    10451111
    1046     public function test_get_comment_not_approved() {
     1112    /**
     1113     * @dataProvider data_readable_http_methods
     1114     * @ticket 56481
     1115     *
     1116     * @param string $method HTTP method to use.
     1117     */
     1118    public function test_get_comment_not_approved( $method ) {
    10471119        wp_set_current_user( 0 );
    10481120
    1049         $request  = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$hold_id ) );
     1121        $request  = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%d', self::$hold_id ) );
    10501122        $response = rest_get_server()->dispatch( $request );
    10511123        $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
    10521124    }
    10531125
    1054     public function test_get_comment_not_approved_same_user() {
    1055         wp_set_current_user( self::$admin_id );
    1056 
    1057         $request  = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$hold_id ) );
     1126    /**
     1127     * @dataProvider data_readable_http_methods
     1128     * @ticket 56481
     1129     *
     1130     * @param string $method HTTP method to use.
     1131     */
     1132    public function test_get_comment_not_approved_same_user( $method ) {
     1133        wp_set_current_user( self::$admin_id );
     1134
     1135        $request  = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%d', self::$hold_id ) );
    10581136        $response = rest_get_server()->dispatch( $request );
    10591137        $this->assertSame( 200, $response->get_status() );
     
    10991177    }
    11001178
    1101     public function test_get_comment_with_password_without_edit_post_permission() {
     1179    /**
     1180     * @dataProvider data_readable_http_methods
     1181     * @ticket 56481
     1182     *
     1183     * @param string $method HTTP method to use.
     1184     */
     1185    public function test_get_comment_with_password_without_edit_post_permission( $method ) {
    11021186        wp_set_current_user( self::$subscriber_id );
    11031187
     
    11091193        $password_comment = self::factory()->comment->create( $args );
    11101194
    1111         $request  = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $password_comment ) );
     1195        $request  = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%s', $password_comment ) );
    11121196        $response = rest_get_server()->dispatch( $request );
    11131197        $this->assertErrorResponse( 'rest_cannot_read', $response, 403 );
     
    11151199
    11161200    /**
     1201     * @dataProvider data_readable_http_methods
    11171202     * @ticket 38692
    1118      */
    1119     public function test_get_comment_with_password_with_valid_password() {
     1203     * @ticket 56481
     1204     *
     1205     * @param string $method HTTP method to use.
     1206     */
     1207    public function test_get_comment_with_password_with_valid_password( $method ) {
    11201208        wp_set_current_user( self::$subscriber_id );
    11211209
     
    11271215        $password_comment = self::factory()->comment->create( $args );
    11281216
    1129         $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $password_comment ) );
     1217        $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%s', $password_comment ) );
    11301218        $request->set_param( 'password', 'toomanysecrets' );
    11311219
     
    33663454
    33673455    /**
     3456     * @dataProvider data_readable_http_methods
    33683457     * @ticket 42238
    3369      */
    3370     public function test_check_read_post_permission_with_invalid_post_type() {
     3458     * @ticket 56481
     3459     *
     3460     * @param string $method HTTP method to use.
     3461     */
     3462    public function test_check_read_post_permission_with_invalid_post_type( $method ) {
    33713463        register_post_type(
    33723464            'bug-post',
     
    33873479
    33883480        wp_set_current_user( self::$admin_id );
    3389         $request  = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id );
     3481        $request  = new WP_REST_Request( $method, '/wp/v2/comments/' . $comment_id );
    33903482        $response = rest_get_server()->dispatch( $request );
    33913483        $this->assertSame( 403, $response->get_status() );
    33923484    }
     3485
     3486    /**
     3487     * @dataProvider data_readable_http_methods
     3488     * @ticket 56481
     3489     *
     3490     * @param string $method HTTP method to use.
     3491     */
     3492    public function test_get_items_only_fetches_ids_for_head_requests( $method ) {
     3493        $is_head_request = 'HEAD' === $method;
     3494        $request         = new WP_REST_Request( $method, '/wp/v2/comments' );
     3495
     3496        $filter = new MockAction();
     3497
     3498        add_filter( 'comments_pre_query', array( $filter, 'filter' ), 10, 2 );
     3499
     3500        $response = rest_get_server()->dispatch( $request );
     3501
     3502        $this->assertSame( 200, $response->get_status() );
     3503        if ( $is_head_request ) {
     3504            $this->assertEmpty( $response->get_data() );
     3505        } else {
     3506            $this->assertNotEmpty( $response->get_data() );
     3507        }
     3508
     3509        $args = $filter->get_args();
     3510        $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' );
     3511        $this->assertInstanceOf( WP_Comment_Query::class, $args[0][1], 'Query parameters were not captured.' );
     3512
     3513        /** @var WP_Comment_Query $query */
     3514        $query = $args[0][1];
     3515
     3516        if ( $is_head_request ) {
     3517            $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' );
     3518            $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only post IDs.' );
     3519            $this->assertArrayHasKey( 'update_comment_meta_cache', $query->query_vars, 'The update_comment_meta_cache key is missing in the query vars.' );
     3520            $this->assertFalse( $query->query_vars['update_comment_meta_cache'], 'The update_comment_meta_cache value should be false for HEAD requests.' );
     3521        } else {
     3522            $this->assertTrue( ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'], 'The fields parameter should not be forced to "ids" for non-HEAD requests.' );
     3523            $this->assertArrayHasKey( 'update_comment_meta_cache', $query->query_vars, 'The update_comment_meta_cache key is missing in the query vars.' );
     3524            $this->assertTrue( $query->query_vars['update_comment_meta_cache'], 'The update_comment_meta_cache value should be true for non-HEAD requests.' );
     3525            return;
     3526        }
     3527
     3528        global $wpdb;
     3529        $comments_table = preg_quote( $wpdb->comments, '/' );
     3530        $pattern        = '/^SELECT\s+SQL_CALC_FOUND_ROWS\s+' . $comments_table . '\.comment_ID\s+FROM\s+' . $comments_table . '\s+WHERE/i';
     3531
     3532        // Assert that the SQL query only fetches the ID column.
     3533        $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' );
     3534    }
     3535
     3536    /**
     3537     * @dataProvider data_readable_http_methods
     3538     * @ticket 56481
     3539     *
     3540     * @param string $method The HTTP method to use.
     3541     */
     3542    public function test_get_item_should_allow_adding_headers_via_filter( $method ) {
     3543        $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%d', self::$approved_id ) );
     3544
     3545        $hook_name = 'rest_prepare_comment';
     3546
     3547        $filter   = new MockAction();
     3548        $callback = array( $filter, 'filter' );
     3549        add_filter( $hook_name, $callback );
     3550        $header_filter = new class() {
     3551            public static function add_custom_header( $response ) {
     3552                $response->header( 'X-Test-Header', 'Test' );
     3553
     3554                return $response;
     3555            }
     3556        };
     3557        add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) );
     3558        $response = rest_get_server()->dispatch( $request );
     3559        remove_filter( $hook_name, $callback );
     3560        remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) );
     3561
     3562        $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
     3563        $headers = $response->get_headers();
     3564        $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' );
     3565        $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' );
     3566        $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' );
     3567        if ( 'HEAD' !== $method ) {
     3568            return null;
     3569        }
     3570        $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' );
     3571    }
    33933572}
Note: See TracChangeset for help on using the changeset viewer.