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
24 changes: 24 additions & 0 deletions app/Config/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,28 @@ class Cache extends BaseConfig
* @var bool|list<string>
*/
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<int>
*/
public array $cacheStatusCodes = [];
}
14 changes: 12 additions & 2 deletions system/Filters/PageCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Cache;

/**
* Page Cache filter
Expand All @@ -28,9 +29,17 @@ class PageCache implements FilterInterface
{
private readonly ResponseCache $pageCache;

public function __construct()
/**
* @var list<int>
*/
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 ?? [];
}

/**
Expand Down Expand Up @@ -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.
Expand Down
172 changes: 172 additions & 0 deletions tests/system/Filters/PageCacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <[email protected]>
*
* 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);
}
}
3 changes: 2 additions & 1 deletion user_guide_src/source/changelogs/v4.7.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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()``
Expand Down Expand Up @@ -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 <web_page_caching_cache_status_codes>` 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.
Expand Down
30 changes: 30 additions & 0 deletions user_guide_src/source/general/caching.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
================

Expand Down
Loading