Skip to content

Commit 839a088

Browse files
committed
Site Health: Improve page cache detection with added header and refined verification logic.
- Use a more precise regular expression for identifying "HIT" statuses to avoid false positives (e.g., "no-hit"). - Add specific verification logic for Varnish's `X-Varnish` header. - Use a `null` value in the header mapping to indicate a header existence check, when no callback is supplied. - Improve documentation with links for information about certain headers. Developed in #10855 Follow-up to [61355], [54043]. Props westonruter, dmsnell. See #63748. Fixes #64370. git-svn-id: https://develop.svn.wordpress.org/trunk@61711 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 053c3a7 commit 839a088

2 files changed

Lines changed: 180 additions & 34 deletions

File tree

src/wp-admin/includes/class-wp-site-health.php

Lines changed: 89 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3468,22 +3468,21 @@ public function is_development_environment() {
34683468
}
34693469

34703470
/**
3471-
* Returns a list of headers and its verification callback to verify if page cache is enabled or not.
3472-
*
3473-
* Note: key is header name and value could be callable function to verify header value.
3474-
* Empty value mean existence of header detect page cache is enabled.
3471+
* Returns a mapping from response headers to an optional callback to verify if page cache is enabled or not.
34753472
*
34763473
* @since 6.1.0
34773474
*
3478-
* @return array List of client caching headers and their (optional) verification callbacks.
3475+
* @return array<string, ?callable> Mapping of page caching headers and their (optional) verification callbacks.
3476+
* A null value means a simple existence check is used for the header.
34793477
*/
3480-
public function get_page_cache_headers() {
3478+
public function get_page_cache_headers(): array {
34813479

34823480
$cache_hit_callback = static function ( $header_value ) {
3483-
return str_contains( strtolower( $header_value ), 'hit' );
3481+
return 1 === preg_match( '/(^| |,)HIT(,| |$)/i', $header_value );
34843482
};
34853483

34863484
$cache_headers = array(
3485+
// Standard HTTP caching headers.
34873486
'cache-control' => static function ( $header_value ) {
34883487
return (bool) preg_match( '/max-age=[1-9]/', $header_value );
34893488
},
@@ -3493,36 +3492,107 @@ public function get_page_cache_headers() {
34933492
'age' => static function ( $header_value ) {
34943493
return is_numeric( $header_value ) && $header_value > 0;
34953494
},
3496-
'last-modified' => '',
3497-
'etag' => '',
3495+
'last-modified' => null,
3496+
'etag' => null,
3497+
'via' => null,
3498+
3499+
/**
3500+
* Custom caching headers.
3501+
*
3502+
* These do not seem to be actually used by any caching layers. There were first introduced in a Site Health
3503+
* test in the AMP plugin. They were copied into the Performance Lab plugin's Site Health test before they
3504+
* were merged into core.
3505+
*
3506+
* @link https://github.com/ampproject/amp-wp/pull/6849
3507+
* @link https://github.com/WordPress/performance/pull/263
3508+
* @link https://core.trac.wordpress.org/changeset/54043
3509+
*/
34983510
'x-cache-enabled' => static function ( $header_value ) {
3499-
return 'true' === strtolower( $header_value );
3511+
return ( 'true' === strtolower( $header_value ) );
35003512
},
35013513
'x-cache-disabled' => static function ( $header_value ) {
35023514
return ( 'on' !== strtolower( $header_value ) );
35033515
},
3504-
'x-srcache-store-status' => $cache_hit_callback,
3505-
'x-srcache-fetch-status' => $cache_hit_callback,
35063516

3507-
// Generic caching proxies (Nginx, Varnish, etc.)
3517+
/**
3518+
* CloudFlare.
3519+
*
3520+
* @link https://developers.cloudflare.com/cache/concepts/cache-responses/
3521+
*/
3522+
'cf-cache-status' => $cache_hit_callback,
3523+
3524+
/**
3525+
* Fastly.
3526+
*
3527+
* @link https://www.fastly.com/documentation/reference/http/http-headers/X-Cache/
3528+
*/
35083529
'x-cache' => $cache_hit_callback,
3509-
'x-cache-status' => $cache_hit_callback,
3530+
3531+
/**
3532+
* LightSpeed.
3533+
*
3534+
* @link https://docs.litespeedtech.com/lscache/devguide/controls/#x-litespeed-cache
3535+
*/
35103536
'x-litespeed-cache' => $cache_hit_callback,
3537+
3538+
/**
3539+
* OpenResty srcache-nginx-module.
3540+
*
3541+
* The `x-srcache-store-status` header indicates if the response was stored in the cache.
3542+
* Valid values include `STORE` and `BYPASS`.
3543+
*
3544+
* The `x-srcache-fetch-status` header indicates if the response was fetched from the cache.
3545+
* Valid values include `HIT`, `MISS`, and `BYPASS`.
3546+
*
3547+
* @link https://github.com/openresty/srcache-nginx-module
3548+
*/
3549+
'x-srcache-store-status' => static function ( $header_value ) {
3550+
return 'store' === strtolower( $header_value );
3551+
},
3552+
'x-srcache-fetch-status' => $cache_hit_callback,
3553+
3554+
/**
3555+
* Nginx.
3556+
*
3557+
* @link https://blog.nginx.org/blog/nginx-caching-guide
3558+
* @link https://www.inmotionhosting.com/support/website/nginx-cache-management/
3559+
*/
3560+
'x-cache-status' => $cache_hit_callback,
35113561
'x-proxy-cache' => $cache_hit_callback,
3512-
'via' => '',
35133562

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

35183578
/**
35193579
* Filters the list of cache headers supported by core.
35203580
*
3581+
* This list indicates how each of the specified headers will be checked to indicate if a page cache is enabled
3582+
* or not. WordPress checks for each of the headers in the returned array. If the callback is provided, it will
3583+
* be passed the value for the corresponding header and return a boolean value indicating if the header suggests
3584+
* that a cache is active. If the value is `null` for the header, then WordPress will assume that a cache is
3585+
* active if the header is present, regardless of its value.
3586+
*
35213587
* @since 6.1.0
35223588
*
3523-
* @param array $cache_headers Array of supported cache headers.
3589+
* @param array<string, ?callable> $cache_headers Mapping from cache-related HTTP headers to whether they
3590+
* indicate if a page cache is enabled for the site. `null`
3591+
* indicates caching in the presence of the header; a callback is
3592+
* provided the header’s value and should return `true` if it
3593+
* implies that a cache is active.
35243594
*/
3525-
return apply_filters( 'site_status_page_cache_supported_cache_headers', $cache_headers );
3595+
return (array) apply_filters( 'site_status_page_cache_supported_cache_headers', $cache_headers );
35263596
}
35273597

35283598
/**

tests/phpunit/tests/admin/wpSiteHealth.php

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ class Tests_Admin_wpSiteHealth extends WP_UnitTestCase {
1212
* An instance of the class to test.
1313
*
1414
* @since 6.1.0
15-
*
16-
* @var WP_Site_Health
1715
*/
18-
private $instance;
16+
private WP_Site_Health $instance;
1917

2018
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
2119
// Include the `WP_Site_Health` file.
@@ -172,7 +170,7 @@ public function data_cron_health_checks() {
172170
* @covers ::get_page_cache_headers()
173171
* @covers ::check_for_page_caching()
174172
*/
175-
public function test_get_page_cache( $responses, $expected_status, $expected_label, $good_basic_auth = null, $delay_the_response = false ) {
173+
public function test_get_page_cache( array $responses, string $expected_status, string $expected_label, bool $has_basic_auth = false, bool $delay_the_response = false ) {
176174
$expected_props = array(
177175
'badge' => array(
178176
'label' => __( 'Performance' ),
@@ -183,7 +181,7 @@ public function test_get_page_cache( $responses, $expected_status, $expected_lab
183181
'label' => $expected_label,
184182
);
185183

186-
if ( null !== $good_basic_auth ) {
184+
if ( $has_basic_auth ) {
187185
$_SERVER['PHP_AUTH_USER'] = 'admin';
188186
$_SERVER['PHP_AUTH_PW'] = 'password';
189187
}
@@ -200,7 +198,7 @@ static function () use ( $threshold ) {
200198

201199
add_filter(
202200
'pre_http_request',
203-
function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $good_basic_auth, $delay_the_response, $threshold ) {
201+
function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $has_basic_auth, $delay_the_response, $threshold ) {
204202

205203
$expected_response = array_shift( $responses );
206204

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

222-
if ( null !== $good_basic_auth ) {
220+
if ( $has_basic_auth ) {
223221
$this->assertArrayHasKey(
224222
'Authorization',
225223
$parsed_args['headers']
@@ -263,9 +261,15 @@ function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $good
263261
*
264262
* @ticket 56041
265263
*
266-
* @return array[]
264+
* @return array<string, array{
265+
* responses: array<int, string|array<string, string|string[]>>,
266+
* expected_status: 'recommended'|'critical'|'good',
267+
* expected_label: string,
268+
* good_basic_auth?: bool,
269+
* delay_the_response?: bool,
270+
* }>
267271
*/
268-
public function data_get_page_cache() {
272+
public function data_get_page_cache(): array {
269273
$recommended_label = 'Page cache is not detected but the server response time is OK';
270274
$good_label = 'Page cache is detected and the server response time is good';
271275
$critical_label = 'Page cache is not detected and the server response time is slow';
@@ -278,13 +282,13 @@ public function data_get_page_cache() {
278282
),
279283
'expected_status' => 'recommended',
280284
'expected_label' => $error_label,
281-
'good_basic_auth' => false,
285+
'has_basic_auth' => true,
282286
),
283287
'no-cache-control' => array(
284288
'responses' => array_fill( 0, 3, array() ),
285289
'expected_status' => 'critical',
286290
'expected_label' => $critical_label,
287-
'good_basic_auth' => null,
291+
'has_basic_auth' => false,
288292
'delay_the_response' => true,
289293
),
290294
'no-cache' => array(
@@ -310,7 +314,7 @@ public function data_get_page_cache() {
310314
'responses' => array_fill( 0, 3, array( 'cache-control' => 'no-cache' ) ),
311315
'expected_status' => 'critical',
312316
'expected_label' => $critical_label,
313-
'good_basic_auth' => null,
317+
'has_basic_auth' => false,
314318
'delay_the_response' => true,
315319
),
316320
'age' => array(
@@ -366,7 +370,7 @@ public function data_get_page_cache() {
366370
),
367371
'expected_status' => 'critical',
368372
'expected_label' => $critical_label,
369-
'good_basic_auth' => null,
373+
'has_basic_auth' => false,
370374
'delay_the_response' => true,
371375
),
372376
'cache-control-with-basic-auth' => array(
@@ -377,7 +381,7 @@ public function data_get_page_cache() {
377381
),
378382
'expected_status' => 'good',
379383
'expected_label' => $good_label,
380-
'good_basic_auth' => true,
384+
'has_basic_auth' => true,
381385
),
382386
'x-cache-enabled' => array(
383387
'responses' => array_fill(
@@ -396,7 +400,7 @@ public function data_get_page_cache() {
396400
),
397401
'expected_status' => 'critical',
398402
'expected_label' => $critical_label,
399-
'good_basic_auth' => null,
403+
'has_basic_auth' => false,
400404
'delay_the_response' => true,
401405
),
402406
'x-cache-disabled' => array(
@@ -408,6 +412,78 @@ public function data_get_page_cache() {
408412
'expected_status' => 'good',
409413
'expected_label' => $good_label,
410414
),
415+
'false-positive-hit-in-word' => array(
416+
'responses' => array_fill(
417+
0,
418+
3,
419+
array( 'x-cache' => 'no-hit' )
420+
),
421+
'expected_status' => 'recommended',
422+
'expected_label' => $recommended_label,
423+
),
424+
'varnish-header' => array(
425+
'responses' => array_fill(
426+
0,
427+
3,
428+
array( 'x-varnish' => '123 456' )
429+
),
430+
'expected_status' => 'good',
431+
'expected_label' => $good_label,
432+
),
433+
'varnish-header-miss' => array(
434+
'responses' => array_fill(
435+
0,
436+
3,
437+
array( 'x-varnish' => '123' )
438+
),
439+
'expected_status' => 'recommended',
440+
'expected_label' => $recommended_label,
441+
),
442+
'srcache-store-status' => array(
443+
'responses' => array_fill(
444+
0,
445+
3,
446+
array( 'x-srcache-store-status' => 'STORE' )
447+
),
448+
'expected_status' => 'good',
449+
'expected_label' => $good_label,
450+
),
451+
'srcache-store-status-bypass' => array(
452+
'responses' => array_fill(
453+
0,
454+
3,
455+
array( 'x-srcache-store-status' => 'BYPASS' )
456+
),
457+
'expected_status' => 'recommended',
458+
'expected_label' => $recommended_label,
459+
),
460+
'srcache-fetch-status' => array(
461+
'responses' => array_fill(
462+
0,
463+
3,
464+
array( 'x-srcache-fetch-status' => 'HIT' )
465+
),
466+
'expected_status' => 'good',
467+
'expected_label' => $good_label,
468+
),
469+
'last-modified' => array(
470+
'responses' => array_fill(
471+
0,
472+
3,
473+
array( 'last-modified' => 'Wed, 21 Oct 2015 07:28:00 GMT' )
474+
),
475+
'expected_status' => 'good',
476+
'expected_label' => $good_label,
477+
),
478+
'via' => array(
479+
'responses' => array_fill(
480+
0,
481+
3,
482+
array( 'via' => '1.1 varnish' )
483+
),
484+
'expected_status' => 'good',
485+
'expected_label' => $good_label,
486+
),
411487
);
412488
}
413489

0 commit comments

Comments
 (0)