diff --git a/app/Config/Cache.php b/app/Config/Cache.php index f2077c9d29b0..a8e3e1f053ff 100644 --- a/app/Config/Cache.php +++ b/app/Config/Cache.php @@ -169,4 +169,28 @@ class Cache extends BaseConfig * @var bool|list */ public $cacheQueryString = false; + + /** + * -------------------------------------------------------------------------- + * Web Page Caching: Cache Status Codes + * -------------------------------------------------------------------------- + * + * HTTP status codes that are allowed to be cached. Only responses with + * these status codes will be cached by the PageCache filter. + * + * Default: [] - Cache all status codes (backward compatible) + * + * Recommended: [200] - Only cache successful responses + * + * You can also use status codes like: + * [200, 404, 410] - Cache successful responses and specific error codes + * [200, 201, 202, 203, 204] - All 2xx successful responses + * + * WARNING: Using [] may cache temporary error pages (404, 500, etc). + * Consider restricting to [200] for production applications to avoid + * caching errors that should be temporary. + * + * @var list + */ + public array $cacheStatusCodes = []; } diff --git a/system/Filters/PageCache.php b/system/Filters/PageCache.php index 7a2949802723..c170e0d46cac 100644 --- a/system/Filters/PageCache.php +++ b/system/Filters/PageCache.php @@ -20,6 +20,7 @@ use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; +use Config\Cache; /** * Page Cache filter @@ -28,9 +29,17 @@ class PageCache implements FilterInterface { private readonly ResponseCache $pageCache; - public function __construct() + /** + * @var list + */ + private readonly array $cacheStatusCodes; + + public function __construct(?Cache $config = null) { - $this->pageCache = service('responsecache'); + $config ??= config('Cache'); + + $this->pageCache = service('responsecache'); + $this->cacheStatusCodes = $config->cacheStatusCodes ?? []; } /** @@ -61,6 +70,7 @@ public function after(RequestInterface $request, ResponseInterface $response, $a if ( ! $response instanceof DownloadResponse && ! $response instanceof RedirectResponse + && ($this->cacheStatusCodes === [] || in_array($response->getStatusCode(), $this->cacheStatusCodes, true)) ) { // Cache it without the performance metrics replaced // so that we can have live speed updates along the way. diff --git a/tests/system/Filters/PageCacheTest.php b/tests/system/Filters/PageCacheTest.php new file mode 100644 index 000000000000..baf46fc28cac --- /dev/null +++ b/tests/system/Filters/PageCacheTest.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\DownloadResponse; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\Response; +use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\HTTP\SiteURI; +use CodeIgniter\HTTP\UserAgent; +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; +use Config\Cache; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('Others')] +final class PageCacheTest extends CIUnitTestCase +{ + private function createRequest(): IncomingRequest + { + $superglobals = service('superglobals'); + $superglobals->setServer('REQUEST_URI', '/'); + + $siteUri = new SiteURI(new App()); + + return new IncomingRequest(new App(), $siteUri, null, new UserAgent()); + } + + public function testDefaultConfigCachesAllStatusCodes(): void + { + $config = new Cache(); + $filter = new PageCache($config); + + $request = $this->createRequest(); + + $response200 = new Response(new App()); + $response200->setStatusCode(200); + $response200->setBody('Success'); + + $result = $filter->after($request, $response200); + $this->assertInstanceOf(Response::class, $result); + + $response404 = new Response(new App()); + $response404->setStatusCode(404); + $response404->setBody('Not Found'); + + $result = $filter->after($request, $response404); + $this->assertInstanceOf(Response::class, $result); + + $response500 = new Response(new App()); + $response500->setStatusCode(500); + $response500->setBody('Server Error'); + + $result = $filter->after($request, $response500); + $this->assertInstanceOf(Response::class, $result); + } + + public function testRestrictedConfigOnlyCaches200Responses(): void + { + $config = new Cache(); + $config->cacheStatusCodes = [200]; + $filter = new PageCache($config); + + $request = $this->createRequest(); + + // Test 200 response - should be cached + $response200 = new Response(new App()); + $response200->setStatusCode(200); + $response200->setBody('Success'); + + $result = $filter->after($request, $response200); + $this->assertInstanceOf(Response::class, $result); + + // Test 404 response - should NOT be cached + $response404 = new Response(new App()); + $response404->setStatusCode(404); + $response404->setBody('Not Found'); + + $result = $filter->after($request, $response404); + $this->assertNotInstanceOf(ResponseInterface::class, $result); + + // Test 500 response - should NOT be cached + $response500 = new Response(new App()); + $response500->setStatusCode(500); + $response500->setBody('Server Error'); + + $result = $filter->after($request, $response500); + $this->assertNotInstanceOf(ResponseInterface::class, $result); + } + + public function testCustomCacheStatusCodes(): void + { + $config = new Cache(); + $config->cacheStatusCodes = [200, 404, 410]; + $filter = new PageCache($config); + + $request = $this->createRequest(); + + $response200 = new Response(new App()); + $response200->setStatusCode(200); + $response200->setBody('Success'); + + $result = $filter->after($request, $response200); + $this->assertInstanceOf(Response::class, $result); + + $response404 = new Response(new App()); + $response404->setStatusCode(404); + $response404->setBody('Not Found'); + + $result = $filter->after($request, $response404); + $this->assertInstanceOf(Response::class, $result); + + $response410 = new Response(new App()); + $response410->setStatusCode(410); + $response410->setBody('Gone'); + + $result = $filter->after($request, $response410); + $this->assertInstanceOf(Response::class, $result); + + // Test 500 response - should NOT be cached (not in whitelist) + $response500 = new Response(new App()); + $response500->setStatusCode(500); + $response500->setBody('Server Error'); + + $result = $filter->after($request, $response500); + $this->assertNotInstanceOf(ResponseInterface::class, $result); + } + + public function testDownloadResponseNotCached(): void + { + $config = new Cache(); + $config->cacheStatusCodes = [200]; + $filter = new PageCache($config); + + $request = $this->createRequest(); + + $response = new DownloadResponse('test.txt', true); + + $result = $filter->after($request, $response); + $this->assertNotInstanceOf(ResponseInterface::class, $result); + } + + public function testRedirectResponseNotCached(): void + { + $config = new Cache(); + $config->cacheStatusCodes = [200, 301, 302]; + $filter = new PageCache($config); + + $request = $this->createRequest(); + + $response = new RedirectResponse(new App()); + $response->redirect('/new-url'); + + $result = $filter->after($request, $response); + $this->assertNotInstanceOf(ResponseInterface::class, $result); + } +} diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index ac950e8123ad..7ccbabd684cc 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -121,7 +121,7 @@ Method Signature Changes ======================== - **BaseModel:** The type of the ``$row`` parameter for the ``cleanValidationRules()`` method has been changed from ``?array $row = null`` to ``array $row``. - +- **PageCache:** The ``PageCache`` filter constructor now accepts an optional ``Cache`` configuration parameter: ``__construct(?Cache $config = null)``. This allows dependency injection for testing purposes. While this is technically a breaking change if you extend the ``PageCache`` class with your own constructor, it should not affect most users as the parameter has a default value. - Added the ``SensitiveParameter`` attribute to various methods to conceal sensitive information from stack traces. Affected methods are: - ``CodeIgniter\Encryption\EncrypterInterface::encrypt()`` - ``CodeIgniter\Encryption\EncrypterInterface::decrypt()`` @@ -168,6 +168,7 @@ Libraries - **Cache:** Added ``async`` and ``persistent`` config item to Predis handler. - **Cache:** Added ``persistent`` config item to Redis handler. - **Cache:** Added support for HTTP status in ``ResponseCache``. +- **Cache:** Added ``Config\Cache::$cacheStatusCodes`` to control which HTTP status codes are allowed to be cached by the ``PageCache`` filter. Defaults to ``[]`` (all status codes for backward compatibility). Recommended value: ``[200]`` to only cache successful responses. See :ref:`Setting $cacheStatusCodes ` for details. - **CURLRequest:** Added ``shareConnection`` config item to change default share connection. - **CURLRequest:** Added ``dns_cache_timeout`` option to change default DNS cache timeout. - **CURLRequest:** Added ``fresh_connect`` options to enable/disable request fresh connection. diff --git a/user_guide_src/source/general/caching.rst b/user_guide_src/source/general/caching.rst index 23856561b0c0..a5a92e29fe6d 100644 --- a/user_guide_src/source/general/caching.rst +++ b/user_guide_src/source/general/caching.rst @@ -63,6 +63,36 @@ Valid options are: - **array**: Enabled, but only take into account the specified list of query parameters. E.g., ``['q', 'page']``. +.. _web_page_caching_cache_status_codes: + +Setting $cacheStatusCodes +------------------------- + +.. versionadded:: 4.7.0 + +You can control which HTTP response status codes are allowed to be cached +with ``Config\Cache::$cacheStatusCodes``. + +Valid options are: + +- ``[]``: (default) Cache all HTTP status codes. This maintains backward + compatibility but may cache temporary error pages. +- ``[200]``: (Recommended) Only cache successful responses. This prevents + caching of error pages (404, 500, etc.) that should be temporary. +- array of status codes: Cache only specific status codes. For example: + + - ``[200, 404]``: Cache successful responses and not found pages. + - ``[200, 404, 410]``: Cache successful responses and specific error codes. + - ``[200, 201, 202, 203, 204]``: All 2xx successful responses. + +.. warning:: Using an empty array ``[]`` may cache temporary error pages (404, 500, etc). + For production applications, consider restricting this to ``[200]`` to avoid + caching errors that should be temporary. For example, a cached 404 page would + remain cached even after the resource is created, until the cache expires. + +.. note:: Regardless of this setting, ``DownloadResponse`` and ``RedirectResponse`` + instances are never cached by the ``PageCache`` filter. + Enabling Caching ================