From e1171cb55efa16051ad688d09bbbfa55d0f330fd Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 21 Jul 2026 12:10:25 +0530 Subject: [PATCH 1/7] Add finishResponse function in ResponseTrait and trigger post_response event after sending response --- system/CodeIgniter.php | 2 ++ system/HTTP/ResponseTrait.php | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 16fa7bc1b0f5..40b2fe064a9a 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -297,6 +297,8 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon $this->sendResponse(); + Events::trigger('post_response'); + return null; } diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index b3c0c88da326..3fe428c5a7ff 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -385,9 +385,34 @@ public function send() $this->sendCookies(); $this->sendBody(); + $this->finishResponse(); + return $this; } + private function finishResponse(): void + { + if (function_exists('fastcgi_finish_request')) { + fastcgi_finish_request(); + } elseif (function_exists('litespeed_finish_request')) { + litespeed_finish_request(); + } elseif (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { + $this->closeOutputBuffers(); + flush(); + } + } + + private function closeOutputBuffers(): void + { + $status = ob_get_status(true); + $level = count($status); + $flags = PHP_OUTPUT_HANDLER_REMOVABLE | PHP_OUTPUT_HANDLER_FLUSHABLE; + + while ($level-- > 0 && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { + ob_end_flush(); + } + } + /** * Decides whether {@see ContentSecurityPolicy::finalize()} should run for * this response. Keeping the CSP class unloaded on requests that do not From 1fbc16989b61fee9039963628980f6310ef2a185 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 21 Jul 2026 12:10:40 +0530 Subject: [PATCH 2/7] Add docs for the new post_response event --- user_guide_src/source/changelogs/v4.8.0.rst | 1 + user_guide_src/source/extending/events.rst | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index b03c3c4f3b23..29c89428f200 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -350,6 +350,7 @@ Others - **Filters:** Added ``RequestId`` filter for request tracing and correlation logging. The filter stores the request ID in the request context and automatically adds the ``X-Request-ID`` response header. Incoming ``X-Request-ID`` headers are used when valid. See :ref:`requestid` for details. - **Environment:** Added ``CodeIgniter\EnvironmentDetector`` class and corresponding ``environment`` service as a mockable wrapper around the ``ENVIRONMENT`` constant. Framework internals that previously compared ``ENVIRONMENT`` directly now go through this service, making environment-specific branches reachable in tests via ``Services::injectMock()``. See :ref:`environment-detector-service`. +- **Events**: Added ``post_response`` event to run after the response is sent to the client. - **Time:** Added ``Time::between()``, ``Time::min()``, and ``Time::max()`` comparison helpers. See :ref:`between `, :ref:`min `, and :ref:`max `. *************** diff --git a/user_guide_src/source/extending/events.rst b/user_guide_src/source/extending/events.rst index b628247e1dbe..2f449f31e860 100644 --- a/user_guide_src/source/extending/events.rst +++ b/user_guide_src/source/extending/events.rst @@ -99,6 +99,10 @@ invoked by **public/index.php**: * **post_controller_constructor** Called immediately after your controller is instantiated, but prior to any method calls happening. * **post_system** Called right before the final rendered page is sent to the browser, at the end of system execution, after the execution of "after" controller filters. +* **post_response** Called after the response is sent to the client. This event is useful + for performing tasks that do not need to be completed before the response is sent, such as logging or cleanup tasks. + +.. note:: The ``post_response`` event was added in v4.8.0. .. _event-points-for-cli-apps: From cdd8b19f5670ba2b3735f31f08c6aaa699dd7171 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 21 Jul 2026 19:35:43 +0530 Subject: [PATCH 3/7] Add tests --- .../Responses/ResponseWithPostSendFlag.php | 24 +++++++++++++++++++ tests/system/CodeIgniterTest.php | 22 +++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/_support/HTTP/Responses/ResponseWithPostSendFlag.php diff --git a/tests/_support/HTTP/Responses/ResponseWithPostSendFlag.php b/tests/_support/HTTP/Responses/ResponseWithPostSendFlag.php new file mode 100644 index 000000000000..c2e723aa320f --- /dev/null +++ b/tests/_support/HTTP/Responses/ResponseWithPostSendFlag.php @@ -0,0 +1,24 @@ +responseSent = true; + + return $this; + } +} diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 0feb97f555ca..467d690f9c92 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -17,6 +17,7 @@ use CodeIgniter\Config\Factories; use CodeIgniter\Config\Services; use CodeIgniter\Debug\Timer; +use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\Method; use CodeIgniter\HTTP\Response; @@ -39,6 +40,7 @@ use Tests\Support\Filters\Customfilter; use Tests\Support\Filters\RedirectFilter; use Tests\Support\Router\Filters\TestAttributeFilter; +use Tests\Support\HTTP\Responses\ResponseWithPostSendFlag; /** * @internal @@ -1328,4 +1330,24 @@ public function testResetForWorkerModeDoesNotLoadCspWhenDisabled(): void $this->assertNull(RichRenderer::$css_nonce); $this->assertTrue(RichRenderer::$needs_pre_render); } + + public function testPostResponseTriggeredAfterSend(): void + { + $this->resetServices(); + + $response = new ResponseWithPostSendFlag(); + Services::injectMock('response', $response); + + $postResponseTriggeredAfterResponseSent = false; + + Events::on('post_response', static function () use (&$postResponseTriggeredAfterResponseSent, &$response) { + $postResponseTriggeredAfterResponseSent = $response->responseSent; + }); + + ob_start(); + $this->codeigniter->run(); + ob_get_clean(); + + $this->assertTrue($postResponseTriggeredAfterResponseSent); + } } From 0ee4b076955440b4a5f27c29e4d84c17b36deab8 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 21 Jul 2026 19:40:14 +0530 Subject: [PATCH 4/7] cs-fix --- system/HTTP/ResponseTrait.php | 8 ++++---- .../HTTP/Responses/ResponseWithPostSendFlag.php | 11 +++++++++++ tests/system/CodeIgniterTest.php | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 3fe428c5a7ff..9c980b75a256 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -396,7 +396,7 @@ private function finishResponse(): void fastcgi_finish_request(); } elseif (function_exists('litespeed_finish_request')) { litespeed_finish_request(); - } elseif (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { + } elseif (! in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { $this->closeOutputBuffers(); flush(); } @@ -405,10 +405,10 @@ private function finishResponse(): void private function closeOutputBuffers(): void { $status = ob_get_status(true); - $level = count($status); - $flags = PHP_OUTPUT_HANDLER_REMOVABLE | PHP_OUTPUT_HANDLER_FLUSHABLE; + $level = count($status); + $flags = PHP_OUTPUT_HANDLER_REMOVABLE | PHP_OUTPUT_HANDLER_FLUSHABLE; - while ($level-- > 0 && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { + while ($level-- > 0 && ($s = $status[$level]) && (! isset($s['del']) ? ! isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { ob_end_flush(); } } diff --git a/tests/_support/HTTP/Responses/ResponseWithPostSendFlag.php b/tests/_support/HTTP/Responses/ResponseWithPostSendFlag.php index c2e723aa320f..980d1a6154bf 100644 --- a/tests/_support/HTTP/Responses/ResponseWithPostSendFlag.php +++ b/tests/_support/HTTP/Responses/ResponseWithPostSendFlag.php @@ -1,5 +1,16 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace Tests\Support\HTTP\Responses; use CodeIgniter\HTTP\Response; diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 467d690f9c92..0be17307c9ba 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -39,8 +39,8 @@ use PHPUnit\Framework\Attributes\WithoutErrorHandler; use Tests\Support\Filters\Customfilter; use Tests\Support\Filters\RedirectFilter; -use Tests\Support\Router\Filters\TestAttributeFilter; use Tests\Support\HTTP\Responses\ResponseWithPostSendFlag; +use Tests\Support\Router\Filters\TestAttributeFilter; /** * @internal @@ -1340,7 +1340,7 @@ public function testPostResponseTriggeredAfterSend(): void $postResponseTriggeredAfterResponseSent = false; - Events::on('post_response', static function () use (&$postResponseTriggeredAfterResponseSent, &$response) { + Events::on('post_response', static function () use (&$postResponseTriggeredAfterResponseSent, &$response): void { $postResponseTriggeredAfterResponseSent = $response->responseSent; }); From 9378dff9c3a9fb22efdba80be850e04c5206362f Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 21 Jul 2026 20:01:39 +0530 Subject: [PATCH 5/7] Retain original Symphony behaviour --- system/HTTP/ResponseTrait.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 9c980b75a256..360c308ff6e3 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -405,10 +405,10 @@ private function finishResponse(): void private function closeOutputBuffers(): void { $status = ob_get_status(true); - $level = count($status); - $flags = PHP_OUTPUT_HANDLER_REMOVABLE | PHP_OUTPUT_HANDLER_FLUSHABLE; + $level = count($status); + $flags = PHP_OUTPUT_HANDLER_REMOVABLE | PHP_OUTPUT_HANDLER_FLUSHABLE; - while ($level-- > 0 && ($s = $status[$level]) && (! isset($s['del']) ? ! isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { + while ($level-- > 0 && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { ob_end_flush(); } } From ed1c5355c94358094f458d427ecfeb9f6593c4b8 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 21 Jul 2026 20:05:34 +0530 Subject: [PATCH 6/7] cs-fix --- system/HTTP/ResponseTrait.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 360c308ff6e3..9c980b75a256 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -405,10 +405,10 @@ private function finishResponse(): void private function closeOutputBuffers(): void { $status = ob_get_status(true); - $level = count($status); - $flags = PHP_OUTPUT_HANDLER_REMOVABLE | PHP_OUTPUT_HANDLER_FLUSHABLE; + $level = count($status); + $flags = PHP_OUTPUT_HANDLER_REMOVABLE | PHP_OUTPUT_HANDLER_FLUSHABLE; - while ($level-- > 0 && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { + while ($level-- > 0 && ($s = $status[$level]) && (! isset($s['del']) ? ! isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { ob_end_flush(); } } From 92f88bcf3fd2931763e0c9e7648eb895822e41e0 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 21 Jul 2026 20:19:43 +0530 Subject: [PATCH 7/7] fix rector --- system/HTTP/ResponseTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 9c980b75a256..3b052a8304e4 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -408,7 +408,7 @@ private function closeOutputBuffers(): void $level = count($status); $flags = PHP_OUTPUT_HANDLER_REMOVABLE | PHP_OUTPUT_HANDLER_FLUSHABLE; - while ($level-- > 0 && ($s = $status[$level]) && (! isset($s['del']) ? ! isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { + while ($level-- > 0 && ($s = $status[$level]) && ($s['del'] ?? (! isset($s['flags']) || ($s['flags'] & $flags) === $flags))) { ob_end_flush(); } }