Skip to content

Commit d2ecfc9

Browse files
committed
feat: add configurable status code filtering for PageCache filter
1 parent fe1e944 commit d2ecfc9

File tree

5 files changed

+241
-3
lines changed

5 files changed

+241
-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: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
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(string $uri = 'test/page'): IncomingRequest
35+
{
36+
$superglobals = service('superglobals');
37+
$superglobals->setServer('REQUEST_URI', '/' . $uri);
38+
$superglobals->setServer('SCRIPT_NAME', '/index.php');
39+
40+
$siteUri = new SiteURI(new App(), $uri);
41+
42+
return new IncomingRequest(new App(), $siteUri, null, new UserAgent());
43+
}
44+
45+
public function testDefaultConfigCachesAllStatusCodes(): void
46+
{
47+
$config = new Cache();
48+
$filter = new PageCache($config);
49+
50+
$request = $this->createRequest();
51+
52+
$response200 = new Response(new App());
53+
$response200->setStatusCode(200);
54+
$response200->setBody('Success');
55+
56+
$result = $filter->after($request, $response200);
57+
$this->assertInstanceOf(Response::class, $result);
58+
59+
$response404 = new Response(new App());
60+
$response404->setStatusCode(404);
61+
$response404->setBody('Not Found');
62+
63+
$result = $filter->after($request, $response404);
64+
$this->assertInstanceOf(Response::class, $result);
65+
66+
$response500 = new Response(new App());
67+
$response500->setStatusCode(500);
68+
$response500->setBody('Server Error');
69+
70+
$result = $filter->after($request, $response500);
71+
$this->assertInstanceOf(Response::class, $result);
72+
}
73+
74+
public function testRestrictedConfigOnlyCaches200Responses(): void
75+
{
76+
$config = new Cache();
77+
$config->cacheStatusCodes = [200];
78+
$filter = new PageCache($config);
79+
80+
$request = $this->createRequest();
81+
82+
// Test 200 response - should be cached
83+
$response200 = new Response(new App());
84+
$response200->setStatusCode(200);
85+
$response200->setBody('Success');
86+
87+
$result = $filter->after($request, $response200);
88+
$this->assertInstanceOf(Response::class, $result);
89+
90+
// Test 404 response - should NOT be cached
91+
$response404 = new Response(new App());
92+
$response404->setStatusCode(404);
93+
$response404->setBody('Not Found');
94+
95+
$result = $filter->after($request, $response404);
96+
$this->assertNotInstanceOf(ResponseInterface::class, $result);
97+
98+
// Test 500 response - should NOT be cached
99+
$response500 = new Response(new App());
100+
$response500->setStatusCode(500);
101+
$response500->setBody('Server Error');
102+
103+
$result = $filter->after($request, $response500);
104+
$this->assertNotInstanceOf(ResponseInterface::class, $result);
105+
}
106+
107+
public function testCustomCacheStatusCodes(): void
108+
{
109+
$config = new Cache();
110+
$config->cacheStatusCodes = [200, 404, 410];
111+
$filter = new PageCache($config);
112+
113+
$request = $this->createRequest();
114+
115+
$response200 = new Response(new App());
116+
$response200->setStatusCode(200);
117+
$response200->setBody('Success');
118+
119+
$result = $filter->after($request, $response200);
120+
$this->assertInstanceOf(Response::class, $result);
121+
122+
$response404 = new Response(new App());
123+
$response404->setStatusCode(404);
124+
$response404->setBody('Not Found');
125+
126+
$result = $filter->after($request, $response404);
127+
$this->assertInstanceOf(Response::class, $result);
128+
129+
$response410 = new Response(new App());
130+
$response410->setStatusCode(410);
131+
$response410->setBody('Gone');
132+
133+
$result = $filter->after($request, $response410);
134+
$this->assertInstanceOf(Response::class, $result);
135+
136+
// Test 500 response - should NOT be cached (not in whitelist)
137+
$response500 = new Response(new App());
138+
$response500->setStatusCode(500);
139+
$response500->setBody('Server Error');
140+
141+
$result = $filter->after($request, $response500);
142+
$this->assertNotInstanceOf(ResponseInterface::class, $result);
143+
}
144+
145+
public function testDownloadResponseNotCached(): void
146+
{
147+
$config = new Cache();
148+
$config->cacheStatusCodes = [200];
149+
$filter = new PageCache($config);
150+
151+
$request = $this->createRequest();
152+
153+
$response = new DownloadResponse('test.txt', true);
154+
155+
$result = $filter->after($request, $response);
156+
$this->assertNotInstanceOf(ResponseInterface::class, $result);
157+
}
158+
159+
public function testRedirectResponseNotCached(): void
160+
{
161+
$config = new Cache();
162+
$config->cacheStatusCodes = [200, 301, 302];
163+
$filter = new PageCache($config);
164+
165+
$request = $this->createRequest();
166+
167+
$response = new RedirectResponse(new App());
168+
$response->redirect('/new-url');
169+
170+
$result = $filter->after($request, $response);
171+
$this->assertNotInstanceOf(ResponseInterface::class, $result);
172+
}
173+
}

user_guide_src/source/changelogs/v4.7.0.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Method Signature Changes
116116
========================
117117

118118
- **BaseModel:** The type of the ``$row`` parameter for the ``cleanValidationRules()`` method has been changed from ``?array $row = null`` to ``array $row``.
119-
119+
- **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.
120120
- Added the ``SensitiveParameter`` attribute to various methods to conceal sensitive information from stack traces. Affected methods are:
121121
- ``CodeIgniter\Encryption\EncrypterInterface::encrypt()``
122122
- ``CodeIgniter\Encryption\EncrypterInterface::decrypt()``
@@ -162,6 +162,7 @@ Libraries
162162
- **CLI:** Added ``SignalTrait`` to provide unified handling of operating system signals in CLI commands.
163163
- **Cache:** Added ``async`` and ``persistent`` config item to Predis handler.
164164
- **Cache:** Added ``persistent`` config item to Redis handler.
165+
- **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.
165166
- **CURLRequest:** Added ``shareConnection`` config item to change default share connection.
166167
- **CURLRequest:** Added ``dns_cache_timeout`` option to change default DNS cache timeout.
167168
- **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)