diff --git a/plugins/performance-lab/includes/site-health/audit-enqueued-assets/helper.php b/plugins/performance-lab/includes/site-health/audit-enqueued-assets/helper.php index b528821a28..393b2dae00 100644 --- a/plugins/performance-lab/includes/site-health/audit-enqueued-assets/helper.php +++ b/plugins/performance-lab/includes/site-health/audit-enqueued-assets/helper.php @@ -524,9 +524,30 @@ function perflab_aea_get_asset_size( string $resource_url ) { ); } - // TODO: A non-cacheable response should also be considered an error. - // TODO: A size of zero could be considered an error too. - return strlen( wp_remote_retrieve_body( $response ) ); + $body = wp_remote_retrieve_body( $response ); + + if ( strlen( $body ) === 0 ) { + return new WP_Error( + 'zero_size', + esc_html__( 'The asset returned an empty response body.', 'performance-lab' ) + ); + } + + $cache_control = wp_remote_retrieve_header( $response, 'cache-control' ); + if ( is_array( $cache_control ) ) { + $cache_control = implode( ', ', $cache_control ); + } + if ( '' !== $cache_control ) { + $directives = array_map( 'trim', explode( ',', strtolower( $cache_control ) ) ); + if ( in_array( 'no-store', $directives, true ) ) { + return new WP_Error( + 'not_cacheable', + esc_html__( 'The asset response has a Cache-Control: no-store directive and will not be cached by browsers.', 'performance-lab' ) + ); + } + } + + return strlen( $body ); } /** diff --git a/plugins/performance-lab/tests/data/class-audit-assets-mock-assets.php b/plugins/performance-lab/tests/data/class-audit-assets-mock-assets.php index 10b9b793ff..f4406d7bbc 100644 --- a/plugins/performance-lab/tests/data/class-audit-assets-mock-assets.php +++ b/plugins/performance-lab/tests/data/class-audit-assets-mock-assets.php @@ -156,6 +156,7 @@ static function ( $preempt, $parsed_args, $url ) { return array( 'response' => self::$mocked_responses[ $url ], 'body' => self::$mocked_responses[ $url ]['body'] ?? '', + 'headers' => self::$mocked_responses[ $url ]['headers'] ?? array(), ); } return $preempt; diff --git a/plugins/performance-lab/tests/includes/site-health/audit-enqueued-assets/test-audit-enqueued-assets-helper.php b/plugins/performance-lab/tests/includes/site-health/audit-enqueued-assets/test-audit-enqueued-assets-helper.php index e3939b8190..68efda134f 100644 --- a/plugins/performance-lab/tests/includes/site-health/audit-enqueued-assets/test-audit-enqueued-assets-helper.php +++ b/plugins/performance-lab/tests/includes/site-health/audit-enqueued-assets/test-audit-enqueued-assets-helper.php @@ -520,10 +520,84 @@ public function test_perflab_aea_get_asset_size(): void { $this->assertWPError( perflab_aea_get_asset_size( 'https://example.com/script1.js' ) ); $this->assertWPError( perflab_aea_get_asset_size( 'https://example.com/script2.js' ) ); - $this->assertEquals( 0, perflab_aea_get_asset_size( 'https://example.com/script3.js' ) ); + $zero_size_result = perflab_aea_get_asset_size( 'https://example.com/script3.js' ); + $this->assertWPError( $zero_size_result ); + $this->assertSame( 'zero_size', $zero_size_result->get_error_code() ); $this->assertEquals( 1000, perflab_aea_get_asset_size( 'https://example.com/script4.js' ) ); } + /** + * Tests perflab_aea_get_asset_size() with a Cache-Control: no-store response. + * + * @covers ::perflab_aea_get_asset_size + */ + public function test_perflab_aea_get_asset_size_not_cacheable(): void { + Audit_Assets_Mock_Assets::clear_mocked(); + Audit_Assets_Mock_Assets::mock_requests( + array( + array( + 'url' => 'https://example.com/no-store.js', + 'response' => array( + 'code' => 200, + 'body' => str_repeat( 'A', 500 ), + 'headers' => array( 'cache-control' => 'no-store' ), + ), + ), + array( + 'url' => 'https://example.com/no-store-with-extras.js', + 'response' => array( + 'code' => 200, + 'body' => str_repeat( 'A', 500 ), + 'headers' => array( 'cache-control' => 'no-store, must-revalidate' ), + ), + ), + array( + 'url' => 'https://example.com/no-cache.js', + 'response' => array( + 'code' => 200, + 'body' => str_repeat( 'A', 500 ), + 'headers' => array( 'cache-control' => 'no-cache' ), + ), + ), + array( + 'url' => 'https://example.com/cacheable.js', + 'response' => array( + 'code' => 200, + 'body' => str_repeat( 'A', 500 ), + 'headers' => array( 'cache-control' => 'max-age=3600' ), + ), + ), + array( + 'url' => 'https://example.com/no-store-array-header.js', + 'response' => array( + 'code' => 200, + 'body' => str_repeat( 'A', 500 ), + 'headers' => array( 'cache-control' => array( 'no-store', 'must-revalidate' ) ), + ), + ), + ) + ); + + $no_store_result = perflab_aea_get_asset_size( 'https://example.com/no-store.js' ); + $this->assertWPError( $no_store_result ); + $this->assertSame( 'not_cacheable', $no_store_result->get_error_code() ); + + $no_store_extras_result = perflab_aea_get_asset_size( 'https://example.com/no-store-with-extras.js' ); + $this->assertWPError( $no_store_extras_result ); + $this->assertSame( 'not_cacheable', $no_store_extras_result->get_error_code() ); + + // no-cache means revalidate, not no-store — should not be flagged. + $this->assertEquals( 500, perflab_aea_get_asset_size( 'https://example.com/no-cache.js' ) ); + + // Normal cacheable response should return size. + $this->assertEquals( 500, perflab_aea_get_asset_size( 'https://example.com/cacheable.js' ) ); + + // Cache-Control header returned as an array (multiple headers) should still be detected. + $no_store_array_result = perflab_aea_get_asset_size( 'https://example.com/no-store-array-header.js' ); + $this->assertWPError( $no_store_array_result ); + $this->assertSame( 'not_cacheable', $no_store_array_result->get_error_code() ); + } + /** * Tests perflab_aea_copy_basic_auth_headers() with various scenarios. *