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

    r59458 r59899  
    628628    }
    629629
    630     public function test_get_terms_pagination_headers() {
     630    /**
     631     * @dataProvider data_readable_http_methods
     632     * @ticket 56481
     633     *
     634     * @param string $method HTTP method to use.
     635     */
     636    public function test_get_terms_pagination_headers( $method ) {
    631637        $total_tags  = self::$total_tags;
    632638        $total_pages = (int) ceil( $total_tags / 10 );
    633639
    634640        // Start of the index.
    635         $request  = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     641        $request  = new WP_REST_Request( $method, '/wp/v2/tags' );
    636642        $response = rest_get_server()->dispatch( $request );
    637643        $headers  = $response->get_headers();
     
    15031509        $this->check_taxonomy_term( $tag, $data, $response->get_links() );
    15041510    }
     1511
     1512    /**
     1513     * @dataProvider data_readable_http_methods
     1514     * @ticket 56481
     1515     *
     1516     * @param string $method HTTP method to use.
     1517     */
     1518    public function test_get_items_only_fetches_ids_for_head_requests( $method ) {
     1519        $is_head_request = 'HEAD' === $method;
     1520        $request         = new WP_REST_Request( $method, '/wp/v2/tags' );
     1521
     1522        $filter = new MockAction();
     1523
     1524        add_filter( 'terms_pre_query', array( $filter, 'filter' ), 10, 2 );
     1525
     1526        $response = rest_get_server()->dispatch( $request );
     1527
     1528        $this->assertSame( 200, $response->get_status() );
     1529        if ( $is_head_request ) {
     1530            $this->assertEmpty( $response->get_data() );
     1531        } else {
     1532            $this->assertNotEmpty( $response->get_data() );
     1533        }
     1534
     1535        $args = $filter->get_args();
     1536        $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' );
     1537        $this->assertInstanceOf( WP_Term_Query::class, $args[0][1], 'Query parameters were not captured.' );
     1538
     1539        /** @var WP_Term_Query $query */
     1540        $query = $args[0][1];
     1541
     1542        if ( $is_head_request ) {
     1543            $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' );
     1544            $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only term IDs.' );
     1545            $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' );
     1546            $this->assertFalse( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be false for HEAD requests.' );
     1547        } else {
     1548            $this->assertTrue(
     1549                ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'],
     1550                'The fields parameter should not be forced to "ids" for non-HEAD requests.'
     1551            );
     1552            $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' );
     1553            $this->assertTrue( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be true for HEAD requests.' );
     1554        }
     1555
     1556        if ( ! $is_head_request ) {
     1557            return;
     1558        }
     1559
     1560        global $wpdb;
     1561        $terms_table = preg_quote( $wpdb->terms, '/' );
     1562
     1563        $pattern = '/SELECT\s+t\.term_id.+FROM\s+' . $terms_table . '\s+AS\s+t\s+INNER\s+JOIN/is';
     1564
     1565        // Assert that the SQL query only fetches the term_id column.
     1566        $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' );
     1567    }
     1568
     1569    /**
     1570     * Data provider intended to provide HTTP method names for testing GET and HEAD requests.
     1571     *
     1572     * @return array
     1573     */
     1574    public static function data_readable_http_methods() {
     1575        return array(
     1576            'GET request'  => array( 'GET' ),
     1577            'HEAD request' => array( 'HEAD' ),
     1578        );
     1579    }
     1580
     1581    /**
     1582     * @dataProvider data_readable_http_methods
     1583     * @ticket 56481
     1584     *
     1585     * @param string $method The HTTP method to use.
     1586     */
     1587    public function test_get_item_should_allow_adding_headers_via_filter( string $method ) {
     1588        $tag_id = self::factory()->tag->create();
     1589
     1590        $request = new WP_REST_Request( $method, sprintf( '/wp/v2/tags/%d', $tag_id ) );
     1591
     1592        $hook_name = 'rest_prepare_post_tag';
     1593
     1594        $filter   = new MockAction();
     1595        $callback = array( $filter, 'filter' );
     1596        add_filter( $hook_name, $callback );
     1597        $header_filter = new class() {
     1598            public static function add_custom_header( $response ) {
     1599                $response->header( 'X-Test-Header', 'Test' );
     1600
     1601                return $response;
     1602            }
     1603        };
     1604        add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) );
     1605        $response = rest_get_server()->dispatch( $request );
     1606        remove_filter( $hook_name, $callback );
     1607        remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) );
     1608
     1609        $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' );
     1610        $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' );
     1611        $headers = $response->get_headers();
     1612        $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' );
     1613        $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' );
     1614        if ( 'HEAD' !== $method ) {
     1615            return null;
     1616        }
     1617        $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' );
     1618    }
    15051619}
Note: See TracChangeset for help on using the changeset viewer.