Repository: codeigniter4/CodeIgniter4 (PHP)

Issue:
Bug: sendResponse doesn't respect returnResponse parameter in codeigniter.php
### PHP Version

8.1

### CodeIgniter4 Version

4.2.7

### CodeIgniter4 Installation Method

Composer (using `codeigniter4/appstarter`)

### Which operating systems have you tested for this bug?

Linux

### Which server did you use?

cli

### Database

MariaDB 10.2

### What happened?

File in the question: `system/CodeIgniter.php`

The run() method (line: 292) in the file has the second parameter to return the response instead of printing it.
However, in the case of  page not found exception, the script straight prints the result.
Which makes it harder to cache the exception response in the caller script.

Indeed there are other ways to deal with the issue but all of them require extending a few methods in the child class (extending `Codeigniter.php`) just to include a return statement.

### Steps to Reproduce

1. Use a non-traditional server like [ReactPHP](https://reactphp.org/) or [Swoole](https://github.com/swoole).
2. Initiate requests from the server to CodeIgniter.
```php
$app = Services::codeigniter(config('App'));
$app->initialize();
$app->setContext('php-cli');
```
3. Execute the `run()` method along with `$returnResponse` set to true.
`$app->run(null, true);`
4. Override default 404 page with a controller/method.
5. Request a resource that doesn't exist.
6. In the case of a response code 200, CI will return the HTTP\Response object.
7. In the case of a response code 404, CI will return null and the result (HTML formatted response) will be written to the server's log file.

I haven't tested but the behavior might be seen in the case of response code 500 as well.

### Expected Output

When there is a Page not found exception, the server should return the response object instead of printing (echoing) it. 
Same is true when a internal server error occurs.

### Anything else?

I have extended the core class to overcome this issue but I strongly believe that the framework should respect the type of return requested for either of the HTTP response codes.

Hence it, would be better to make `$returnResponse` as a class property and change the `sendResponse()` method's behavior.

Otherwise below are lines to update in the `system/CodeIgniter.php` file to let the the application return the response when it is asked rather than echoing when a page not found or server error exceptions occur.

Line 312 in the run() method
`return $this->sendResponse($returnResponse);`

Line 348 in the run() method
`return $this->sendResponse($returnResponse);`

Line 354 pass the parameter
`$this->display404errors($e, $returnResponse);`

Line 914 method signature
`protected function display404errors(PageNotFoundException $e, bool $returnResponse = false)`

Line 937 in the display404errors() method
`return $this->sendResponse($returnResponse);`

Line 1072 sendResponse() method
```php
    protected function sendResponse($returnResponse = false)
    {
        if (!$returnResponse) {
			$this->response->pretend($this->useSafeOutput)->send();
			return;
		}
		return $this->response;
    }
```


Discussion:
Thank you for reporting.
Why don't you send a PR?
See https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md
I didn't test the changes yet when a server error occurs or there is a redirection exception.
Hence, I don't think it would be right to send the PR at the moment without following all specifications for the same.
Thanks
I see.

By the way, do you make it to run CI4 on [ReactPHP](https://reactphp.org/) or [Swoole](https://github.com/swoole)?

Not fully yet. It is [Open Swoole](https://openswoole.com).

I'm still exercising to populate the HTTP/IncomingRequest object from the server's request object.
I  would recommend you see https://github.com/monkenWu/CodeIgniter4-Burner

I would like to support Long-living PHP like Swoole, but it looks like a long long way.
Hi, I'm glad @kenjis mentioned my Library.

I had a lot of problems, as you did, not only with the Request and Response object conversion, but after that you will have problems with the Debug bar, SESSION and file upload; The point of view from the long-living PHP applications, the CodeIgniter4 is not well designed. This is because CodeIgniter4 is not designed to stay in memory.

I have been spending my spare time researching how to support long-living PHP applications without changing the CodeIgniter4 source code. I've had some good results with CodeIgniter4-Burner so far, so feel free to discuss it with me.

Thanks @kenjis for mentioning the library and the contributors to giving the faster frameworks.
I don't think it would be wise to carry discussion over a bug ticket hence I have further asked from monkenWu on the linked issue.

There is a feedback for CI team as well.
@monkenWu I really appreciate your great work. I have not tried Burner yet, but I would like to look into it soon.

I have started a thread in the Forum to exchange information.
https://forum.codeigniter.com/showthread.php?tid=83970

@AmitSonkhiya Could you please try out @kenjis PR linked above and see if it fixes this issue for you?
@MGatner 
I would do. Just give me 3 days to get back at work.
Thanks for the update.
@MGatner @kenjis 

Thanks for the update.
This bug doesn't appear anymore using CI from the development branch.
Great! Thanks for confirming @AmitSonkhiya 



Patch:
diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php
index 0cc4861be854..a8d1c8fa7b91 100644
--- a/system/CodeIgniter.php
+++ b/system/CodeIgniter.php
@@ -151,6 +151,11 @@ class CodeIgniter
      */
     protected ?string $context = null;
 
+    /**
+     * Whether to return Response object or send response.
+     */
+    protected bool $returnResponse = false;
+
     /**
      * Constructor.
      */
@@ -291,6 +296,8 @@ protected function initializeKint()
      */
     public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
     {
+        $this->returnResponse = $returnResponse;
+
         if ($this->context === null) {
             throw new LogicException('Context must be set before run() is called. If you are upgrading from 4.1.x, you need to merge `public/index.php` and `spark` file from `vendor/codeigniter4/framework`.');
         }
@@ -309,6 +316,10 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon
         if ($this->request instanceof IncomingRequest && strtolower($this->request->getMethod()) === 'cli') {
             $this->response->setStatusCode(405)->setBody('Method Not Allowed');
 
+            if ($this->returnResponse) {
+                return $this->response;
+            }
+
             $this->sendResponse();
 
             return;
@@ -345,13 +356,22 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon
             // If the route is a 'redirect' route, it throws
             // the exception with the $to as the message
             $this->response->redirect(base_url($e->getMessage()), 'auto', $e->getCode());
+
+            if ($this->returnResponse) {
+                return $this->response;
+            }
+
             $this->sendResponse();
 
             $this->callExit(EXIT_SUCCESS);
 
             return;
         } catch (PageNotFoundException $e) {
-            $this->display404errors($e);
+            $return = $this->display404errors($e);
+
+            if ($return instanceof ResponseInterface) {
+                return $return;
+            }
         }
     }
 
@@ -400,9 +420,13 @@ private function isWeb(): bool
      *
      * @throws PageNotFoundException
      * @throws RedirectException
+     *
+     * @deprecated $returnResponse is deprecated.
      */
     protected function handleRequest(?RouteCollectionInterface $routes, Cache $cacheConfig, bool $returnResponse = false)
     {
+        $this->returnResponse = $returnResponse;
+
         $routeFilter = $this->tryToRouteIt($routes);
 
         $uri = $this->determinePath();
@@ -433,7 +457,8 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
 
             // If a ResponseInterface instance is returned then send it back to the client and stop
             if ($possibleResponse instanceof ResponseInterface) {
-                return $returnResponse ? $possibleResponse : $possibleResponse->pretend($this->useSafeOutput)->send();
+                return $this->returnResponse ? $possibleResponse
+                    : $possibleResponse->pretend($this->useSafeOutput)->send();
             }
 
             if ($possibleResponse instanceof Request) {
@@ -512,7 +537,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
 
         unset($uri);
 
-        if (! $returnResponse) {
+        if (! $this->returnResponse) {
             $this->sendResponse();
         }
 
@@ -910,6 +935,8 @@ protected function runController($class)
     /**
      * Displays a 404 Page Not Found error. If set, will try to
      * call the 404Override controller/method that was set in routing config.
+     *
+     * @return ResponseInterface|void
      */
     protected function display404errors(PageNotFoundException $e)
     {
@@ -934,6 +961,10 @@ protected function display404errors(PageNotFoundException $e)
 
             $cacheConfig = new Cache();
             $this->gatherOutput($cacheConfig, $returned);
+            if ($this->returnResponse) {
+                return $this->response;
+            }
+
             $this->sendResponse();
 
             return;
diff --git a/user_guide_src/source/changelogs/v4.2.8.rst b/user_guide_src/source/changelogs/v4.2.8.rst
index 2d69f6c709ef..f8451958d2a3 100644
--- a/user_guide_src/source/changelogs/v4.2.8.rst
+++ b/user_guide_src/source/changelogs/v4.2.8.rst
@@ -31,7 +31,7 @@ none.
 Deprecations
 ************
 
-none.
+- The third parameter ``$returnResponse`` of ``CodeIgniter::handleRequest()`` is deprecated.
 
 Bugs Fixed
 **********


Test patch:
diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php
index 4535096ccc61..fee7759bb264 100644
--- a/tests/system/CodeIgniterTest.php
+++ b/tests/system/CodeIgniterTest.php
@@ -66,6 +66,16 @@ public function testRunEmptyDefaultRoute()
         $this->assertStringContainsString('Welcome to CodeIgniter', $output);
     }
 
+    public function testRunEmptyDefaultRouteReturnResponse()
+    {
+        $_SERVER['argv'] = ['index.php'];
+        $_SERVER['argc'] = 1;
+
+        $response = $this->codeigniter->useSafeOutput(true)->run(null, true);
+
+        $this->assertStringContainsString('Welcome to CodeIgniter', $response->getBody());
+    }
+
     public function testRunClosureRoute()
     {
         $_SERVER['argv'] = ['index.php', 'pages/about'];
@@ -78,7 +88,7 @@ public function testRunClosureRoute()
         $routes->add('pages/(:segment)', static function ($segment) {
             echo 'You want to see "' . esc($segment) . '" page.';
         });
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -97,7 +107,7 @@ public function testRun404Override()
         $routes = Services::routes();
         $routes->setAutoRoute(false);
         $routes->set404Override('Tests\Support\Controllers\Hello::index');
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -116,7 +126,7 @@ public function testRun404OverrideControllerReturnsResponse()
         $routes = Services::routes();
         $routes->setAutoRoute(false);
         $routes->set404Override('Tests\Support\Controllers\Popcorn::pop');
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -126,6 +136,23 @@ public function testRun404OverrideControllerReturnsResponse()
         $this->assertStringContainsString('Oops', $output);
     }
 
+    public function testRun404OverrideReturnResponse()
+    {
+        $_SERVER['argv'] = ['index.php', '/'];
+        $_SERVER['argc'] = 2;
+
+        // Inject mock router.
+        $routes = Services::routes();
+        $routes->setAutoRoute(false);
+        $routes->set404Override('Tests\Support\Controllers\Popcorn::pop');
+        $router = Services::router($routes, Services::incomingrequest());
+        Services::injectMock('router', $router);
+
+        $response = $this->codeigniter->useSafeOutput(true)->run($routes, true);
+
+        $this->assertStringContainsString('Oops', $response->getBody());
+    }
+
     public function testRun404OverrideByClosure()
     {
         $_SERVER['argv'] = ['index.php', '/'];
@@ -137,7 +164,7 @@ public function testRun404OverrideByClosure()
         $routes->set404Override(static function () {
             echo '404 Override by Closure.';
         });
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -157,7 +184,7 @@ public function testControllersCanReturnString()
         // Inject mock router.
         $routes = Services::routes();
         $routes->add('pages/(:segment)', static fn ($segment) => 'You want to see "' . esc($segment) . '" page.');
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -182,7 +209,7 @@ public function testControllersCanReturnResponseObject()
 
             return $response->setBody($string);
         });
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -209,7 +236,7 @@ public function testControllersCanReturnDownloadResponseObject()
 
             return $response->download('some.txt', 'some text', true);
         });
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -228,9 +255,9 @@ public function testControllersRunFilterByClassName()
 
         // Inject mock router.
         $routes = Services::routes();
-        $routes->add('pages/about', static fn () => Services::request()->getBody(), ['filter' => Customfilter::class]);
+        $routes->add('pages/about', static fn () => Services::incomingrequest()->getBody(), ['filter' => Customfilter::class]);
 
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -258,7 +285,7 @@ public function testRoutesIsEmpty()
         $_SERVER['argc'] = 2;
 
         // Inject mock router.
-        $router = Services::router(null, Services::request(), false);
+        $router = Services::router(null, Services::incomingrequest(), false);
         Services::injectMock('router', $router);
 
         ob_start();
@@ -335,7 +362,7 @@ public function testRunRedirectionWithNamed()
         }, ['as' => 'name']);
         $routes->addRedirect('example', 'name');
 
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -358,7 +385,7 @@ public function testRunRedirectionWithURI()
         });
         $routes->addRedirect('example', 'pages/uri');
 
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -382,7 +409,7 @@ public function testRunRedirectionWithURINotSet()
         $routes = Services::routes();
         $routes->addRedirect('example', 'pages/notset');
 
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -405,7 +432,7 @@ public function testRunRedirectionWithHTTPCode303()
         $routes = Services::routes();
         $routes->addRedirect('example', 'pages/notset', 301);
 
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -422,7 +449,7 @@ public function testStoresPreviousURL()
         $_SERVER['argc'] = 2;
 
         // Inject mock router.
-        $router = Services::router(null, Services::request(), false);
+        $router = Services::router(null, Services::incomingrequest(), false);
         Services::injectMock('router', $router);
 
         ob_start();
@@ -446,7 +473,7 @@ public function testNotStoresPreviousURL()
         $routes = Services::routes();
         $routes->addRedirect('example', 'pages/notset', 301);
 
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -470,7 +497,7 @@ public function testNotStoresPreviousURLByCheckingContentType()
 
             return $response->setContentType('image/jpeg', '');
         });
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         ob_start();
@@ -537,7 +564,7 @@ public function testSpoofRequestMethodCanUsePUT()
         $this->codeigniter->useSafeOutput(true)->run();
         ob_get_clean();
 
-        $this->assertSame('put', Services::request()->getMethod());
+        $this->assertSame('put', Services::incomingrequest()->getMethod());
     }
 
     public function testSpoofRequestMethodCannotUseGET()
@@ -561,7 +588,7 @@ public function testSpoofRequestMethodCannotUseGET()
         $this->codeigniter->useSafeOutput(true)->run();
         ob_get_clean();
 
-        $this->assertSame('post', Services::request()->getMethod());
+        $this->assertSame('post', Services::incomingrequest()->getMethod());
     }
 
     /**
@@ -588,7 +615,7 @@ public function testPageCacheSendSecureHeaders()
 
             return $response->setBody($string);
         });
-        $router = Services::router($routes, Services::request());
+        $router = Services::router($routes, Services::incomingrequest());
         Services::injectMock('router', $router);
 
         /** @var Filters $filterConfig */
@@ -619,7 +646,7 @@ public function testPageCacheSendSecureHeaders()
         // Clear Page cache
         command('cache:clear');
 
-        // Remove stream fliters
+        // Remove stream filters
         stream_filter_remove($outputStreamFilter);
         stream_filter_remove($errorStreamFilter);
     }
@@ -654,7 +681,7 @@ public function testPageCacheWithCacheQueryString($cacheQueryStringValue, int $e
             $_SERVER['REQUEST_URI'] = '/' . $testingUrl;
             $routes                 = Services::routes(true);
             $routes->add($testingUrl, static function () {
-                CodeIgniter::cache(0); // Dont cache the page in the run() function because CodeIgniter class will create default $cacheConfig and overwrite settings from the dataProvider
+                CodeIgniter::cache(0); // Don't cache the page in the run() function because CodeIgniter class will create default $cacheConfig and overwrite settings from the dataProvider
                 $response = Services::response();
                 $string   = 'This is a test page, to check cache configuration';
 
@@ -662,7 +689,7 @@ public function testPageCacheWithCacheQueryString($cacheQueryStringValue, int $e
             });
 
             // Inject router
-            $router = Services::router($routes, Services::request(null, false));
+            $router = Services::router($routes, Services::incomingrequest(null, false));
             Services::injectMock('router', $router);
 
             // Cache the page output using default caching function and $cacheConfig with value from the data provider