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

    r59630 r59899  
    163163    }
    164164
    165     public function test_get_items_no_permission() {
     165    /**
     166     * @ticket 56481
     167     */
     168    public function test_get_items_with_head_request_should_not_prepare_revisions_data() {
     169        wp_set_current_user( self::$editor_id );
     170
     171        $hook_name = 'rest_prepare_revision';
     172        $filter    = new MockAction();
     173        $callback  = array( $filter, 'filter' );
     174
     175        add_filter( $hook_name, $callback );
     176        $request  = new WP_REST_Request( 'HEAD', '/wp/v2/posts/' . self::$post_id . '/revisions' );
     177        $response = rest_get_server()->dispatch( $request );
     178        remove_filter( $hook_name, $callback );
     179
     180        $this->assertNotWPError( $response );
     181        $response = rest_ensure_response( $response );
     182
     183        $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
     184        $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' );
     185        $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' );
     186    }
     187
     188    /**
     189     * @dataProvider data_readable_http_methods
     190     * @ticket 56481
     191     *
     192     * @param string $method The HTTP method to use.
     193     */
     194    public function test_get_items_no_permission( $method ) {
    166195        wp_set_current_user( 0 );
    167         $request  = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' );
     196        $request  = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' );
    168197        $response = rest_get_server()->dispatch( $request );
    169198
     
    174203    }
    175204
    176     public function test_get_items_missing_parent() {
    177         wp_set_current_user( self::$editor_id );
    178         $request  = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' );
     205    /**
     206     * Data provider intended to provide HTTP method names for testing GET and HEAD requests.
     207     *
     208     * @return array
     209     */
     210    public static function data_readable_http_methods() {
     211        return array(
     212            'GET request'  => array( 'GET' ),
     213            'HEAD request' => array( 'HEAD' ),
     214        );
     215    }
     216
     217    /**
     218     * @dataProvider data_readable_http_methods
     219     * @ticket 56481
     220     *
     221     * @param string $method The HTTP method to use.
     222     */
     223    public function test_get_items_missing_parent( $method ) {
     224        wp_set_current_user( self::$editor_id );
     225        $request  = new WP_REST_Request( $method, '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' );
    179226        $response = rest_get_server()->dispatch( $request );
    180227        $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
    181228    }
    182229
    183     public function test_get_items_invalid_parent_post_type() {
    184         wp_set_current_user( self::$editor_id );
    185         $request  = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$page_id . '/revisions' );
     230    /**
     231     * @dataProvider data_readable_http_methods
     232     * @ticket 56481
     233     *
     234     * @param string $method The HTTP method to use.
     235     */
     236    public function test_get_items_invalid_parent_post_type( $method ) {
     237        wp_set_current_user( self::$editor_id );
     238        $request  = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$page_id . '/revisions' );
    186239        $response = rest_get_server()->dispatch( $request );
    187240        $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
     
    214267    }
    215268
     269    /**
     270     * @dataProvider data_readable_http_methods
     271     * @ticket 56481
     272     *
     273     * @param string $method The HTTP method to use.
     274     */
     275    public function test_get_item_should_allow_adding_headers_via_filter( $method ) {
     276        wp_set_current_user( self::$editor_id );
     277        $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 );
     278
     279        $hook_name = 'rest_prepare_revision';
     280        $filter    = new MockAction();
     281        $callback  = array( $filter, 'filter' );
     282        add_filter( $hook_name, $callback );
     283        $header_filter = new class() {
     284            public static function add_custom_header( $response ) {
     285                $response->header( 'X-Test-Header', 'Test' );
     286
     287                return $response;
     288            }
     289        };
     290        add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) );
     291        $response = rest_get_server()->dispatch( $request );
     292        remove_filter( $hook_name, $callback );
     293        remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) );
     294
     295        $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
     296        $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' );
     297        $headers = $response->get_headers();
     298        $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' );
     299        $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' );
     300        if ( 'GET' === $method ) {
     301            return null;
     302        }
     303        $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' );
     304    }
     305
    216306    public function test_get_item_embed_context() {
    217307        wp_set_current_user( self::$editor_id );
     
    232322    }
    233323
    234     public function test_get_item_no_permission() {
     324    /**
     325     * @dataProvider data_readable_http_methods
     326     * @ticket 56481
     327     *
     328     * @param string $method The HTTP method to use.
     329     */
     330    public function test_get_item_no_permission( $method ) {
    235331        wp_set_current_user( 0 );
    236         $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 );
     332        $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 );
    237333
    238334        $response = rest_get_server()->dispatch( $request );
     
    243339    }
    244340
    245     public function test_get_item_missing_parent() {
    246         wp_set_current_user( self::$editor_id );
    247         $request  = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions/' . $this->revision_id1 );
     341    /**
     342     * @dataProvider data_readable_http_methods
     343     * @ticket 56481
     344     *
     345     * @param string $method The HTTP method to use.
     346     */
     347    public function test_get_item_missing_parent( $method ) {
     348        wp_set_current_user( self::$editor_id );
     349        $request  = new WP_REST_Request( $method, '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions/' . $this->revision_id1 );
    248350        $response = rest_get_server()->dispatch( $request );
    249351        $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
    250352    }
    251353
    252     public function test_get_item_invalid_parent_post_type() {
    253         wp_set_current_user( self::$editor_id );
    254         $request  = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$page_id . '/revisions/' . $this->revision_id1 );
     354    /**
     355     * @dataProvider data_readable_http_methods
     356     * @ticket 56481
     357     *
     358     * @param string $method The HTTP method to use.
     359     */
     360    public function test_get_item_invalid_parent_post_type( $method ) {
     361        wp_set_current_user( self::$editor_id );
     362        $request  = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$page_id . '/revisions/' . $this->revision_id1 );
    255363        $response = rest_get_server()->dispatch( $request );
    256364        $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
     
    270378
    271379    /**
     380     * @dataProvider data_readable_http_methods
    272381     * @ticket 59875
    273      */
    274     public function test_get_item_invalid_parent_id() {
    275         wp_set_current_user( self::$editor_id );
    276         $request  = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_2_1_id );
     382     * @ticket 56481
     383     *
     384     * @param string $method The HTTP method to use.
     385     */
     386    public function test_get_item_invalid_parent_id( $method ) {
     387        wp_set_current_user( self::$editor_id );
     388        $request  = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_2_1_id );
    277389        $response = rest_get_server()->dispatch( $request );
    278390        $this->assertErrorResponse( 'rest_revision_parent_id_mismatch', $response, 404 );
     
    511623     * Test the pagination header of the first page.
    512624     *
    513      * @ticket 40510
    514      */
    515     public function test_get_items_pagination_header_of_the_first_page() {
     625     * @dataProvider data_readable_http_methods
     626     * @ticket 40510
     627     * @ticket 56481
     628     *
     629     * @param string $method The HTTP method to use.
     630     */
     631    public function test_get_items_pagination_header_of_the_first_page( $method ) {
    516632        wp_set_current_user( self::$editor_id );
    517633
     
    521637        $page        = 1;  // First page.
    522638
    523         $request = new WP_REST_Request( 'GET', $rest_route );
     639        $request = new WP_REST_Request( $method, $rest_route );
    524640        $request->set_query_params(
    525641            array(
     
    546662     * Test the pagination header of the last page.
    547663     *
    548      * @ticket 40510
    549      */
    550     public function test_get_items_pagination_header_of_the_last_page() {
     664     * @dataProvider data_readable_http_methods
     665     * @ticket 40510
     666     * @ticket 56481
     667     *
     668     * @param string $method The HTTP method to use.
     669     */
     670    public function test_get_items_pagination_header_of_the_last_page( $method ) {
    551671        wp_set_current_user( self::$editor_id );
    552672
     
    556676        $page        = 2;  // Last page.
    557677
    558         $request = new WP_REST_Request( 'GET', $rest_route );
     678        $request = new WP_REST_Request( $method, $rest_route );
    559679        $request->set_query_params(
    560680            array(
     
    577697    }
    578698
     699
    579700    /**
    580701     * Test that invalid 'per_page' query should error.
    581702     *
    582      * @ticket 40510
    583      */
    584     public function test_get_items_invalid_per_page_should_error() {
     703     * @dataProvider data_readable_http_methods
     704     * @ticket 40510
     705     * @ticket 56481
     706     *
     707     * @param string $method The HTTP method to use.
     708     */
     709    public function test_get_items_invalid_per_page_should_error( $method ) {
    585710        wp_set_current_user( self::$editor_id );
    586711
     
    589714        $expected_status = 400;
    590715
    591         $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' );
     716        $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' );
    592717        $request->set_param( 'per_page', $per_page );
    593718        $response = rest_get_server()->dispatch( $request );
     
    598723     * Test that out of bounds 'page' query should error.
    599724     *
    600      * @ticket 40510
    601      */
    602     public function test_get_items_out_of_bounds_page_should_error() {
     725     * @dataProvider data_readable_http_methods
     726     * @ticket 40510
     727     * @ticket 56481
     728     *
     729     * @param string $method The HTTP method to use.
     730     */
     731    public function test_get_items_out_of_bounds_page_should_error( $method ) {
    603732        wp_set_current_user( self::$editor_id );
    604733
     
    609738        $expected_status = 400;
    610739
    611         $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' );
     740        $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' );
    612741        $request->set_query_params(
    613742            array(
     
    623752     * Test that impossibly high 'page' query should error.
    624753     *
    625      * @ticket 40510
    626      */
    627     public function test_get_items_invalid_max_pages_should_error() {
     754     * @dataProvider data_readable_http_methods
     755     * @ticket 40510
     756     * @ticket 56481
     757     *
     758     * @param string $method The HTTP method to use.
     759     */
     760    public function test_get_items_invalid_max_pages_should_error( $method ) {
    628761        wp_set_current_user( self::$editor_id );
    629762
     
    633766        $expected_status = 400;
    634767
    635         $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' );
     768        $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' );
    636769        $request->set_query_params(
    637770            array(
     
    771904     * Test that out of bound 'offset' query should error.
    772905     *
    773      * @ticket 40510
    774      */
    775     public function test_get_items_out_of_bound_offset_should_error() {
     906     * @dataProvider data_readable_http_methods
     907     * @ticket 40510
     908     * @ticket 56481
     909     *
     910     * @param string $method The HTTP method to use.
     911     */
     912    public function test_get_items_out_of_bound_offset_should_error( $method ) {
    776913        wp_set_current_user( self::$editor_id );
    777914
     
    781918        $expected_status = 400;
    782919
    783         $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' );
     920        $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' );
    784921        $request->set_query_params(
    785922            array(
     
    795932     * Test that impossible high number for 'offset' query should error.
    796933     *
    797      * @ticket 40510
    798      */
    799     public function test_get_items_impossible_high_number_offset_should_error() {
     934     * @dataProvider data_readable_http_methods
     935     * @ticket 40510
     936     * @ticket 56481
     937     *
     938     * @param string $method The HTTP method to use.
     939     */
     940    public function test_get_items_impossible_high_number_offset_should_error( $method ) {
    800941        wp_set_current_user( self::$editor_id );
    801942
     
    805946        $expected_status = 400;
    806947
    807         $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' );
     948        $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' );
    808949        $request->set_query_params(
    809950            array(
     
    819960     * Test that invalid 'offset' query should error.
    820961     *
    821      * @ticket 40510
    822      */
    823     public function test_get_items_invalid_offset_should_error() {
     962     * @dataProvider data_readable_http_methods
     963     * @ticket 40510
     964     * @ticket 56481
     965     *
     966     * @param string $method The HTTP method to use.
     967     */
     968    public function test_get_items_invalid_offset_should_error( $method ) {
    824969        wp_set_current_user( self::$editor_id );
    825970
     
    829974        $expected_status = 400;
    830975
    831         $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' );
     976        $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' );
    832977        $request->set_query_params(
    833978            array(
Note: See TracChangeset for help on using the changeset viewer.