From d4817d6e90b64b469f3f66c251e2e7420dc08023 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Feb 2026 09:27:32 -0800 Subject: [PATCH 1/4] Site Health: Improve page cache detection robustness. This commit updates the page cache detection in Site Health by: - Using a stricter regex (\bhit\b) for detecting cache hits in response headers, preventing false positives (e.g., 'shit'). - Adding support for the X-Varnish header, identifying a cache hit when it contains two request IDs. - Adding documentation and source links for the supported headers. - Adding PHPUnit test cases to verify the new detection logic. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../includes/class-wp-site-health.php | 34 ++++++++++++++----- tests/phpunit/tests/admin/wpSiteHealth.php | 27 +++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index dd537296a8655..d2649e031e1d3 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -3390,10 +3390,11 @@ public function is_development_environment() { public function get_page_cache_headers() { $cache_hit_callback = static function ( $header_value ) { - return str_contains( strtolower( $header_value ), 'hit' ); + return (bool) preg_match( '/\bhit\b/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 ); }, @@ -3405,24 +3406,39 @@ public function get_page_cache_headers() { }, 'last-modified' => '', 'etag' => '', + + // Custom caching headers. 'x-cache-enabled' => static function ( $header_value ) { return 'true' === strtolower( $header_value ); }, 'x-cache-disabled' => static function ( $header_value ) { return ( 'on' !== strtolower( $header_value ) ); }, + // OpenResty srcache-nginx-module. 'x-srcache-store-status' => $cache_hit_callback, 'x-srcache-fetch-status' => $cache_hit_callback, - // Generic caching proxies (Nginx, Varnish, etc.) - 'x-cache' => $cache_hit_callback, - 'x-cache-status' => $cache_hit_callback, - 'x-litespeed-cache' => $cache_hit_callback, - 'x-proxy-cache' => $cache_hit_callback, - 'via' => '', + // Generic caching proxies (Nginx, Varnish, etc.). + 'x-cache' => $cache_hit_callback, + 'x-cache-status' => $cache_hit_callback, + 'x-litespeed-cache' => $cache_hit_callback, + 'x-proxy-cache' => $cache_hit_callback, + + /** + * Varnish Cache. + * + * For a cache hit, it includes both the ID of the current request and the ID of the request + * that populated the cache. For a miss, it only includes the current request ID. + * + * @link https://vinyl-cache.org/docs/2.1/faq/http.html + */ + 'x-varnish' => static function ( $header_value ) { + return (bool) preg_match( '/\d+ \d+/', $header_value ); + }, + 'via' => '', - // Cloudflare - 'cf-cache-status' => $cache_hit_callback, + // Cloudflare. + 'cf-cache-status' => $cache_hit_callback, ); /** diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php index 2d32bbb14ec4d..fa2ba9cd74480 100644 --- a/tests/phpunit/tests/admin/wpSiteHealth.php +++ b/tests/phpunit/tests/admin/wpSiteHealth.php @@ -408,6 +408,33 @@ 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' => 'shit' ) + ), + '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, + ), ); } From a2d3183abcb19f41a11c177624cdabd0a19b2c4b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Feb 2026 09:31:28 -0800 Subject: [PATCH 2/4] Site Health: Fix detection for x-srcache-store-status header. The x-srcache-store-status header uses 'STORE' or 'BYPASS', not 'HIT' or 'MISS'. This update ensures that 'STORE' is correctly identified as a positive page cache indicator. Also includes: - Documentation for srcache headers. - Explicit list of generic caching proxies (Squid, Go, Fastly, LiteSpeed) in comments. - Test cases for x-srcache-store-status and x-srcache-fetch-status. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../includes/class-wp-site-health.php | 23 +++++++++++++--- tests/phpunit/tests/admin/wpSiteHealth.php | 27 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index d2649e031e1d3..da2dc0994bb4d 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -3414,14 +3414,31 @@ public function get_page_cache_headers() { 'x-cache-disabled' => static function ( $header_value ) { return ( 'on' !== strtolower( $header_value ) ); }, - // OpenResty srcache-nginx-module. - 'x-srcache-store-status' => $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, - // Generic caching proxies (Nginx, Varnish, etc.). + // Generic caching proxies (Nginx, Varnish, Squid, Go, Fastly, LiteSpeed, etc.). + 'x-cache' => $cache_hit_callback, + 'x-cache-status' => $cache_hit_callback, + 'x-litespeed-cache' => $cache_hit_callback, + 'x-proxy-cache' => $cache_hit_callback, /** diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php index fa2ba9cd74480..f3dfdb3ba2502 100644 --- a/tests/phpunit/tests/admin/wpSiteHealth.php +++ b/tests/phpunit/tests/admin/wpSiteHealth.php @@ -435,6 +435,33 @@ public function data_get_page_cache() { '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, + ), ); } From 9aa2b9417603dae61da271880274cf460accdf77 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Feb 2026 09:50:50 -0800 Subject: [PATCH 3/4] Improve docs and typing for get_page_cache_headers method --- .../includes/class-wp-site-health.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index da2dc0994bb4d..b161afdc3109b 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -3378,16 +3378,13 @@ 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 to 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 Mapping of page caching headers and their (optional) verification callbacks. */ - public function get_page_cache_headers() { + public function get_page_cache_headers(): array { $cache_hit_callback = static function ( $header_value ) { return (bool) preg_match( '/\bhit\b/i', $header_value ); @@ -3404,8 +3401,8 @@ 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, // Custom caching headers. 'x-cache-enabled' => static function ( $header_value ) { @@ -3452,7 +3449,7 @@ public function get_page_cache_headers() { 'x-varnish' => static function ( $header_value ) { return (bool) preg_match( '/\d+ \d+/', $header_value ); }, - 'via' => '', + 'via' => null, // Cloudflare. 'cf-cache-status' => $cache_hit_callback, @@ -3463,9 +3460,9 @@ public function get_page_cache_headers() { * * @since 6.1.0 * - * @param array $cache_headers Array of supported cache headers. + * @param array $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 ); } /** From ed212565a1f8eaaf92c65077fe4c54e396e8da45 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Feb 2026 10:39:40 -0800 Subject: [PATCH 4/4] Tidy test_get_page_cache --- .../includes/class-wp-site-health.php | 4 -- tests/phpunit/tests/admin/wpSiteHealth.php | 48 ++++++++++++++----- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index b161afdc3109b..57fb92a67a5a5 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -3429,13 +3429,9 @@ public function get_page_cache_headers(): array { 'x-srcache-fetch-status' => $cache_hit_callback, // Generic caching proxies (Nginx, Varnish, Squid, Go, Fastly, LiteSpeed, etc.). - 'x-cache' => $cache_hit_callback, - 'x-cache-status' => $cache_hit_callback, - 'x-litespeed-cache' => $cache_hit_callback, - 'x-proxy-cache' => $cache_hit_callback, /** diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php index f3dfdb3ba2502..05761765b327c 100644 --- a/tests/phpunit/tests/admin/wpSiteHealth.php +++ b/tests/phpunit/tests/admin/wpSiteHealth.php @@ -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. @@ -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 ) { + public function test_get_page_cache( array $responses, string $expected_status, string $expected_label, bool $good_basic_auth = false, bool $delay_the_response = false ) { $expected_props = array( 'badge' => array( 'label' => __( 'Performance' ), @@ -183,7 +181,7 @@ public function test_get_page_cache( $responses, $expected_status, $expected_lab 'label' => $expected_label, ); - if ( null !== $good_basic_auth ) { + if ( $good_basic_auth ) { $_SERVER['PHP_AUTH_USER'] = 'admin'; $_SERVER['PHP_AUTH_PW'] = 'password'; } @@ -219,7 +217,7 @@ function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $good ); } - if ( null !== $good_basic_auth ) { + if ( $good_basic_auth ) { $this->assertArrayHasKey( 'Authorization', $parsed_args['headers'] @@ -263,9 +261,15 @@ function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $good * * @ticket 56041 * - * @return array[] + * @return array>, + * 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'; @@ -278,13 +282,13 @@ public function data_get_page_cache() { ), 'expected_status' => 'recommended', 'expected_label' => $error_label, - 'good_basic_auth' => false, + 'good_basic_auth' => true, ), 'no-cache-control' => array( 'responses' => array_fill( 0, 3, array() ), 'expected_status' => 'critical', 'expected_label' => $critical_label, - 'good_basic_auth' => null, + 'good_basic_auth' => false, 'delay_the_response' => true, ), 'no-cache' => array( @@ -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, + 'good_basic_auth' => false, 'delay_the_response' => true, ), 'age' => array( @@ -366,7 +370,7 @@ public function data_get_page_cache() { ), 'expected_status' => 'critical', 'expected_label' => $critical_label, - 'good_basic_auth' => null, + 'good_basic_auth' => false, 'delay_the_response' => true, ), 'cache-control-with-basic-auth' => array( @@ -396,7 +400,7 @@ public function data_get_page_cache() { ), 'expected_status' => 'critical', 'expected_label' => $critical_label, - 'good_basic_auth' => null, + 'good_basic_auth' => false, 'delay_the_response' => true, ), 'x-cache-disabled' => array( @@ -462,6 +466,24 @@ public function data_get_page_cache() { '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, + ), ); }