Skip to content

Commit 455068a

Browse files
authored
feat: add configurable status code filtering for PageCache filter (codeigniter4#9856)
1 parent 0d52f5a commit 455068a

File tree

5 files changed

+240
-3
lines changed

5 files changed

+240
-3
lines changed

app/Config/Cache.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,28 @@ class Cache extends BaseConfig
169169
* @var bool|list<string>
170170
*/
171171
public $cacheQueryString = false;
172+
173+
/**
174+
* --------------------------------------------------------------------------
175+
* Web Page Caching: Cache Status Codes
176+
* --------------------------------------------------------------------------
177+
*
178+
* HTTP status codes that are allowed to be cached. Only responses with
179+
* these status codes will be cached by the PageCache filter.
180+
*
181+
* Default: [] - Cache all status codes (backward compatible)
182+
*
183+
* Recommended: [200] - Only cache successful responses
184+
*
185+
* You can also use status codes like:
186+
* [200, 404, 410] - Cache successful responses and specific error codes
187+
* [200, 201, 202, 203, 204] - All 2xx successful responses
188+
*
189+
* WARNING: Using [] may cache temporary error pages (404, 500, etc).
190+
* Consider restricting to [200] for production applications to avoid
191+
* caching errors that should be temporary.
192+
*
193+
* @var list<int>
194+
*/
195+
public array $cacheStatusCodes = [];
172196
}

system/Filters/PageCache.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use CodeIgniter\HTTP\RedirectResponse;
2121
use CodeIgniter\HTTP\RequestInterface;
2222
use CodeIgniter\HTTP\ResponseInterface;
23+
use Config\Cache;
2324

2425
/**
2526
* Page Cache filter
@@ -28,9 +29,17 @@ class PageCache implements FilterInterface
2829
{
2930
private readonly ResponseCache $pageCache;
3031

31-
public function __construct()
32+
/**
33+
* @var list<int>
34+
*/
35+
private readonly array $cacheStatusCodes;
36+
37+
public function __construct(?Cache $config = null)
3238
{
33-
$this->pageCache = service('responsecache');
39+
$config ??= config('Cache');
40+
41+
$this->pageCache = service('responsecache');
42+
$this->cacheStatusCodes = $config->cacheStatusCodes ?? [];
3443
}
3544

3645
/**
@@ -61,6 +70,7 @@ public function after(RequestInterface $request, ResponseInterface $response, $a
6170
if (
6271
! $response instanceof DownloadResponse
6372
&& ! $response instanceof RedirectResponse
73+
&& ($this->cacheStatusCodes === [] || in_array($response->getStatusCode(), $this->cacheStatusCodes, true))
6474
) {
6575
// Cache it without the performance metrics replaced
6676
// so that we can have live speed updates along the way.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\Filters;
15+
16+
use CodeIgniter\HTTP\DownloadResponse;
17+
use CodeIgniter\HTTP\IncomingRequest;
18+
use CodeIgniter\HTTP\RedirectResponse;
19+
use CodeIgniter\HTTP\Response;
20+
use CodeIgniter\HTTP\ResponseInterface;
21+
use CodeIgniter\HTTP\SiteURI;
22+
use CodeIgniter\HTTP\UserAgent;
23+
use CodeIgniter\Test\CIUnitTestCase;
24+
use Config\App;
25+
use Config\Cache;
26+
use PHPUnit\Framework\Attributes\Group;
27+
28+
/**
29+
* @internal
30+
*/
31+
#[Group('Others')]
32+
final class PageCacheTest extends CIUnitTestCase
33+
{
34+
private function createRequest(): IncomingRequest
35+
{
36+
$superglobals = service('superglobals');
37+
$superglobals->setServer('REQUEST_URI', '/');
38+
39+
$siteUri = new SiteURI(new App());
40+
41+
return new IncomingRequest(new App(), $siteUri, null, new UserAgent());
42+
}
43+
44+
public function testDefaultConfigCachesAllStatusCodes(): void
45+
{
46+
$config = new Cache();
47+
$filter = new PageCache($config);
48+
49+
$request = $this->createRequest();
50+
51+
$response200 = new Response(new App());
52+
$response200->setStatusCode(200);
53+
$response200->setBody('Success');
54+
55+
$result = $filter->after($request, $response200);
56+
$this->assertInstanceOf(Response::class, $result);
57+
58+
$response404 = new Response(new App());
59+
$response404->setStatusCode(404);
60+
$response404->setBody('Not Found');
61+
62+
$result = $filter->after($request, $response404);
63+
$this->assertInstanceOf(Response::class, $result);
64+
65+
$response500 = new Response(new App());
66+
$response500->setStatusCode(500);
67+
$response500->setBody('Server Error');
68+
69+
$result = $filter->after($request, $response500);
70+
$this->assertInstanceOf(Response::class, $result);
71+
}
72+
73+
public function testRestrictedConfigOnlyCaches200Responses(): void
74+
{
75+
$config = new Cache();
76+
$config->cacheStatusCodes = [200];
77+
$filter = new PageCache($config);
78+
79+
$request = $this->createRequest();
80+
81+
// Test 200 response - should be cached
82+
$response200 = new Response(new App());
83+
$response200->setStatusCode(200);
84+
$response200->setBody('Success');
85+
86+
$result = $filter->after($request, $response200);
87+
$this->assertInstanceOf(Response::class, $result);
88+
89+
// Test 404 response - should NOT be cached
90+
$response404 = new Response(new App());
91+
$response404->setStatusCode(404);
92+
$response404->setBody('Not Found');
93+
94+
$result = $filter->after($request, $response404);
95+
$this->assertNotInstanceOf(ResponseInterface::class, $result);
96+
97+
// Test 500 response - should NOT be cached
98+
$response500 = new Response(new App());
99+
$response500->setStatusCode(500);
100+
$response500->setBody('Server Error');
101+
102+
$result = $filter->after($request, $response500);
103+
$this->assertNotInstanceOf(ResponseInterface::class, $result);
104+
}
105+
106+
public function testCustomCacheStatusCodes(): void
107+
{
108+
$config = new Cache();
109+
$config->cacheStatusCodes = [200, 404, 410];
110+
$filter = new PageCache($config);
111+
112+
$request = $this->createRequest();
113+
114+
$response200 = new Response(new App());
115+
$response200->setStatusCode(200);
116+
$response200->setBody('Success');
117+
118+
$result = $filter->after($request, $response200);
119+
$this->assertInstanceOf(Response::class, $result);
120+
121+
$response404 = new Response(new App());
122+
$response404->setStatusCode(404);
123+
$response404->setBody('Not Found');
124+
125+
$result = $filter->after($request, $response404);
126+
$this->assertInstanceOf(Response::class, $result);
127+
128+
$response410 = new Response(new App());
129+
$response410->setStatusCode(410);
130+
$response410->setBody('Gone');
131+
132+
$result = $filter->after($request, $response410);
133+
$this->assertInstanceOf(Response::class, $result);
134+
135+
// Test 500 response - should NOT be cached (not in whitelist)
136+
$response500 = new Response(new App());
137+
$response500->setStatusCode(500);
138+
$response500->setBody('Server Error');
139+
140+
$result = $filter->after($request, $response500);
141+
$this->assertNotInstanceOf(ResponseInterface::class, $result);
142+
}
143+
144+
public function testDownloadResponseNotCached(): void
145+
{
146+
$config = new Cache();
147+
$config->cacheStatusCodes = [200];
148+
$filter = new PageCache($config);
149+
150+
$request = $this->createRequest();
151+
152+
$response = new DownloadResponse('test.txt', true);
153+
154+
$result = $filter->after($request, $response);
155+
$this->assertNotInstanceOf(ResponseInterface::class, $result);
156+
}
157+
158+
public function testRedirectResponseNotCached(): void
159+
{
160+
$config = new Cache();
161+
$config->cacheStatusCodes = [200, 301, 302];
162+
$filter = new PageCache($config);
163+
164+
$request = $this->createRequest();
165+
166+
$response = new RedirectResponse(new App());
167+
$response->redirect('/new-url');
168+
169+
$result = $filter->after($request, $response);
170+
$this->assertNotInstanceOf(ResponseInterface::class, $result);
171+
}
172+
}

user_guide_src/source/changelogs/v4.7.0.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ Method Signature Changes
121121
========================
122122

123123
- **BaseModel:** The type of the ``$row`` parameter for the ``cleanValidationRules()`` method has been changed from ``?array $row = null`` to ``array $row``.
124-
124+
- **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.
125125
- Added the ``SensitiveParameter`` attribute to various methods to conceal sensitive information from stack traces. Affected methods are:
126126
- ``CodeIgniter\Encryption\EncrypterInterface::encrypt()``
127127
- ``CodeIgniter\Encryption\EncrypterInterface::decrypt()``
@@ -171,6 +171,7 @@ Libraries
171171
- **Cache:** Added ``async`` and ``persistent`` config item to Predis handler.
172172
- **Cache:** Added ``persistent`` config item to Redis handler.
173173
- **Cache:** Added support for HTTP status in ``ResponseCache``.
174+
- **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.
174175
- **CURLRequest:** Added ``shareConnection`` config item to change default share connection.
175176
- **CURLRequest:** Added ``dns_cache_timeout`` option to change default DNS cache timeout.
176177
- **CURLRequest:** Added ``fresh_connect`` options to enable/disable request fresh connection.

user_guide_src/source/general/caching.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,36 @@ Valid options are:
6363
- **array**: Enabled, but only take into account the specified list of query
6464
parameters. E.g., ``['q', 'page']``.
6565

66+
.. _web_page_caching_cache_status_codes:
67+
68+
Setting $cacheStatusCodes
69+
-------------------------
70+
71+
.. versionadded:: 4.7.0
72+
73+
You can control which HTTP response status codes are allowed to be cached
74+
with ``Config\Cache::$cacheStatusCodes``.
75+
76+
Valid options are:
77+
78+
- ``[]``: (default) Cache all HTTP status codes. This maintains backward
79+
compatibility but may cache temporary error pages.
80+
- ``[200]``: (Recommended) Only cache successful responses. This prevents
81+
caching of error pages (404, 500, etc.) that should be temporary.
82+
- array of status codes: Cache only specific status codes. For example:
83+
84+
- ``[200, 404]``: Cache successful responses and not found pages.
85+
- ``[200, 404, 410]``: Cache successful responses and specific error codes.
86+
- ``[200, 201, 202, 203, 204]``: All 2xx successful responses.
87+
88+
.. warning:: Using an empty array ``[]`` may cache temporary error pages (404, 500, etc).
89+
For production applications, consider restricting this to ``[200]`` to avoid
90+
caching errors that should be temporary. For example, a cached 404 page would
91+
remain cached even after the resource is created, until the cache expires.
92+
93+
.. note:: Regardless of this setting, ``DownloadResponse`` and ``RedirectResponse``
94+
instances are never cached by the ``PageCache`` filter.
95+
6696
Enabling Caching
6797
================
6898

0 commit comments

Comments
 (0)