Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 78 additions & 19 deletions src/wp-admin/includes/class-wp-site-health.php
Original file line number Diff line number Diff line change
Expand Up @@ -3428,22 +3428,20 @@ public function is_development_environment() {
}

/**
* Returns a list of headers and its verification callback to verify if page cache is enabled or not.
*
* Note: key is header name and value could be callable function to verify header value.
* Empty value mean existence of header detect page cache is enabled.
* Returns a mapping from response headers to an optional callback to verify if page cache is enabled or not.
*
* @since 6.1.0
*
* @return array List of client caching headers and their (optional) verification callbacks.
* @return array<string, ?callable> Mapping of page caching headers and their (optional) verification callbacks.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the previous comment explained what an empty value means, but this change removes that knowledge. was that intentional?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was redundant, but it seems it isn't. How about this:

Suggested change
* @return array<string, ?callable> Mapping of page caching headers and their (optional) verification callbacks.
* @return array<string, ?callable> Mapping of page caching headers and their (optional) verification callbacks.
A null value means a simple existence check is used for the header.

*/
public function get_page_cache_headers() {
public function get_page_cache_headers(): array {

$cache_hit_callback = static function ( $header_value ) {
return str_contains( strtolower( $header_value ), 'hit' );
return 1 === preg_match( '/(^| |,)HIT(,| |$)/i', $header_value );
};

$cache_headers = array(
// Standard HTTP caching headers.
'cache-control' => static function ( $header_value ) {
return (bool) preg_match( '/max-age=[1-9]/', $header_value );
},
Expand All @@ -3453,36 +3451,97 @@ public function get_page_cache_headers() {
'age' => static function ( $header_value ) {
return is_numeric( $header_value ) && $header_value > 0;
},
'last-modified' => '',
'etag' => '',
'last-modified' => null,
'etag' => null,
'via' => null,

/**
* Custom caching headers.
*
* These do not seem to be actually used by any caching layers. There were first introduced in a Site Health
* test in the AMP plugin. They were copied into the Performance Lab plugin's Site Health test before they
* were merged into core.
*
* @link https://github.com/ampproject/amp-wp/pull/6849
* @link https://github.com/WordPress/performance/pull/263
* @link https://core.trac.wordpress.org/changeset/54043
*/
'x-cache-enabled' => static function ( $header_value ) {
return 'true' === strtolower( $header_value );
return ( 'true' === strtolower( $header_value ) );
},
'x-cache-disabled' => static function ( $header_value ) {
return ( 'on' !== strtolower( $header_value ) );
},
Comment on lines +3458 to 3474
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inclined to remove these entirely since it seems they were originally introduced erroneously in the AMP plugin. I can't find any caching layers that actually use them. There are a few hundred mentions of them on GitHub, nevertheless: https://github.com/search?q=%2Fx-cache-(enabled%7Cdisabled)%2F&type=code

'x-srcache-store-status' => $cache_hit_callback,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was incorrect. Now fixed.

'x-srcache-fetch-status' => $cache_hit_callback,

// Generic caching proxies (Nginx, Varnish, etc.)
/**
* CloudFlare.
*
* @link https://developers.cloudflare.com/cache/concepts/cache-responses/
*/
'cf-cache-status' => $cache_hit_callback,

/**
* Fastly.
*
* @link https://www.fastly.com/documentation/reference/http/http-headers/X-Cache/
*/
'x-cache' => $cache_hit_callback,
'x-cache-status' => $cache_hit_callback,

/**
* LightSpeed.
*
* @link https://docs.litespeedtech.com/lscache/devguide/controls/#x-litespeed-cache
*/
'x-litespeed-cache' => $cache_hit_callback,

/**
* OpenResty srcache-nginx-module.
*
* The `x-srcache-store-status` header indicates if the response was stored in the cache.
* Valid values include `STORE` and `BYPASS`.
*
* The `x-srcache-fetch-status` header indicates if the response was fetched from the cache.
* Valid values include `HIT`, `MISS`, and `BYPASS`.
*
* @link https://github.com/openresty/srcache-nginx-module
*/
'x-srcache-store-status' => static function ( $header_value ) {
return 'store' === strtolower( $header_value );
},
'x-srcache-fetch-status' => $cache_hit_callback,

/**
* Nginx.
*
* @link https://blog.nginx.org/blog/nginx-caching-guide
* @link https://www.inmotionhosting.com/support/website/nginx-cache-management/
*/
'x-cache-status' => $cache_hit_callback,
'x-proxy-cache' => $cache_hit_callback,
'via' => '',

// Cloudflare
'cf-cache-status' => $cache_hit_callback,
/**
* Varnish Cache.
*
* A header with a single number indicates it was not cached. If there are two numbers (or more), then this
* indicates the response was cached.
*
* @link https://vinyl-cache.org/docs/2.1/faq/http.html
* @link https://www.fastly.com/documentation/reference/http/http-headers/X-Varnish/
* @link https://www.linuxjournal.com/content/speed-your-web-site-varnish
*/
'x-varnish' => static function ( $header_value ) {
return 1 === preg_match( '/^\d+ \d+/', $header_value );
},
);

/**
* Filters the list of cache headers supported by core.
*
* @since 6.1.0
*
* @param array $cache_headers Array of supported cache headers.
* @param array<string, ?callable> $cache_headers Mapping of page caching headers and their (optional) verification callbacks.
*/
return apply_filters( 'site_status_page_cache_supported_cache_headers', $cache_headers );
return (array) apply_filters( 'site_status_page_cache_supported_cache_headers', $cache_headers );
}

/**
Expand Down
106 changes: 91 additions & 15 deletions tests/phpunit/tests/admin/wpSiteHealth.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ class Tests_Admin_wpSiteHealth extends WP_UnitTestCase {
* An instance of the class to test.
*
* @since 6.1.0
*
* @var WP_Site_Health
*/
private $instance;
private WP_Site_Health $instance;

public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
// Include the `WP_Site_Health` file.
Expand Down Expand Up @@ -172,7 +170,7 @@ public function data_cron_health_checks() {
* @covers ::get_page_cache_headers()
* @covers ::check_for_page_caching()
*/
public function test_get_page_cache( $responses, $expected_status, $expected_label, $good_basic_auth = null, $delay_the_response = false ) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that $good_basic_auth was previously only either null or false, so this is now changed to be an actual boolean with true or false.

public function test_get_page_cache( array $responses, string $expected_status, string $expected_label, bool $has_basic_auth = false, bool $delay_the_response = false ) {
$expected_props = array(
'badge' => array(
'label' => __( 'Performance' ),
Expand All @@ -183,7 +181,7 @@ public function test_get_page_cache( $responses, $expected_status, $expected_lab
'label' => $expected_label,
);

if ( null !== $good_basic_auth ) {
if ( $has_basic_auth ) {
$_SERVER['PHP_AUTH_USER'] = 'admin';
$_SERVER['PHP_AUTH_PW'] = 'password';
}
Expand All @@ -200,7 +198,7 @@ static function () use ( $threshold ) {

add_filter(
'pre_http_request',
function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $good_basic_auth, $delay_the_response, $threshold ) {
function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $has_basic_auth, $delay_the_response, $threshold ) {

$expected_response = array_shift( $responses );

Expand All @@ -219,7 +217,7 @@ function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $good
);
}

if ( null !== $good_basic_auth ) {
if ( $has_basic_auth ) {
$this->assertArrayHasKey(
'Authorization',
$parsed_args['headers']
Expand Down Expand Up @@ -263,9 +261,15 @@ function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $good
*
* @ticket 56041
*
* @return array[]
* @return array<string, array{
* responses: array<int, string|array<string, string|string[]>>,
* expected_status: 'recommended'|'critical'|'good',
* expected_label: string,
* good_basic_auth?: bool,
* delay_the_response?: bool,
* }>
*/
public function data_get_page_cache() {
public function data_get_page_cache(): array {
$recommended_label = 'Page cache is not detected but the server response time is OK';
$good_label = 'Page cache is detected and the server response time is good';
$critical_label = 'Page cache is not detected and the server response time is slow';
Expand All @@ -278,13 +282,13 @@ public function data_get_page_cache() {
),
'expected_status' => 'recommended',
'expected_label' => $error_label,
'good_basic_auth' => false,
'has_basic_auth' => true,
),
'no-cache-control' => array(
'responses' => array_fill( 0, 3, array() ),
'expected_status' => 'critical',
'expected_label' => $critical_label,
'good_basic_auth' => null,
'has_basic_auth' => false,
'delay_the_response' => true,
),
'no-cache' => array(
Expand All @@ -310,7 +314,7 @@ public function data_get_page_cache() {
'responses' => array_fill( 0, 3, array( 'cache-control' => 'no-cache' ) ),
'expected_status' => 'critical',
'expected_label' => $critical_label,
'good_basic_auth' => null,
'has_basic_auth' => false,
'delay_the_response' => true,
),
'age' => array(
Expand Down Expand Up @@ -366,7 +370,7 @@ public function data_get_page_cache() {
),
'expected_status' => 'critical',
'expected_label' => $critical_label,
'good_basic_auth' => null,
'has_basic_auth' => false,
'delay_the_response' => true,
),
'cache-control-with-basic-auth' => array(
Expand All @@ -377,7 +381,7 @@ public function data_get_page_cache() {
),
'expected_status' => 'good',
'expected_label' => $good_label,
'good_basic_auth' => true,
'has_basic_auth' => true,
),
'x-cache-enabled' => array(
'responses' => array_fill(
Expand All @@ -396,7 +400,7 @@ public function data_get_page_cache() {
),
'expected_status' => 'critical',
'expected_label' => $critical_label,
'good_basic_auth' => null,
'has_basic_auth' => false,
'delay_the_response' => true,
),
'x-cache-disabled' => array(
Expand All @@ -408,6 +412,78 @@ public function data_get_page_cache() {
'expected_status' => 'good',
'expected_label' => $good_label,
),
'false-positive-hit-in-word' => array(
'responses' => array_fill(
0,
3,
array( 'x-cache' => 'no-hit' )
),
'expected_status' => 'recommended',
'expected_label' => $recommended_label,
),
'varnish-header' => array(
'responses' => array_fill(
0,
3,
array( 'x-varnish' => '123 456' )
),
'expected_status' => 'good',
'expected_label' => $good_label,
),
'varnish-header-miss' => array(
'responses' => array_fill(
0,
3,
array( 'x-varnish' => '123' )
),
'expected_status' => 'recommended',
'expected_label' => $recommended_label,
),
'srcache-store-status' => array(
'responses' => array_fill(
0,
3,
array( 'x-srcache-store-status' => 'STORE' )
),
'expected_status' => 'good',
'expected_label' => $good_label,
),
'srcache-store-status-bypass' => array(
'responses' => array_fill(
0,
3,
array( 'x-srcache-store-status' => 'BYPASS' )
),
'expected_status' => 'recommended',
'expected_label' => $recommended_label,
),
'srcache-fetch-status' => array(
'responses' => array_fill(
0,
3,
array( 'x-srcache-fetch-status' => 'HIT' )
),
'expected_status' => 'good',
'expected_label' => $good_label,
),
'last-modified' => array(
'responses' => array_fill(
0,
3,
array( 'last-modified' => 'Wed, 21 Oct 2015 07:28:00 GMT' )
),
'expected_status' => 'good',
'expected_label' => $good_label,
),
'via' => array(
'responses' => array_fill(
0,
3,
array( 'via' => '1.1 varnish' )
),
'expected_status' => 'good',
'expected_label' => $good_label,
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because I’m not familiar with all of these systems, and don’t have any in front of me, I am not reviewing or verifying these tests.

I’m trusting that you confirmed that the responses correspond to what those projects actually produce and that the values here are factually representative.

the CommonCrawl archive would potentially be a valuable archive to mine for this kind of header behavior.

);
}

Expand Down
Loading