Skip to content

Commit 9bdd9f5

Browse files
authored
Merge pull request #6228 from kenjis/fix-exceptionHandler-statusCode
fix: exceptionHandler may return invalid HTTP status code
2 parents d94e054 + 9a2a0ce commit 9bdd9f5

File tree

13 files changed

+133
-52
lines changed

13 files changed

+133
-52
lines changed

system/Database/Exceptions/DatabaseException.php

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@
1111

1212
namespace CodeIgniter\Database\Exceptions;
1313

14+
use CodeIgniter\Exceptions\HasExitCodeInterface;
1415
use Error;
1516

16-
class DatabaseException extends Error implements ExceptionInterface
17+
class DatabaseException extends Error implements ExceptionInterface, HasExitCodeInterface
1718
{
18-
/**
19-
* Exit status code
20-
*
21-
* @var int
22-
*/
23-
protected $code = EXIT_DATABASE;
19+
public function getExitCode(): int
20+
{
21+
return EXIT_DATABASE;
22+
}
2423
}

system/Debug/Exceptions.php

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace CodeIgniter\Debug;
1313

1414
use CodeIgniter\API\ResponseTrait;
15+
use CodeIgniter\Exceptions\HasExitCodeInterface;
16+
use CodeIgniter\Exceptions\HTTPExceptionInterface;
1517
use CodeIgniter\Exceptions\PageNotFoundException;
1618
use CodeIgniter\HTTP\CLIRequest;
1719
use CodeIgniter\HTTP\Exceptions\HTTPException;
@@ -312,18 +314,15 @@ protected function maskSensitiveData(&$trace, array $keysToMask, string $path =
312314
*/
313315
protected function determineCodes(Throwable $exception): array
314316
{
315-
$statusCode = abs($exception->getCode());
317+
$statusCode = 500;
318+
$exitStatus = EXIT_ERROR;
316319

317-
if ($statusCode < 100 || $statusCode > 599) {
318-
$exitStatus = $statusCode + EXIT__AUTO_MIN;
319-
320-
if ($exitStatus > EXIT__AUTO_MAX) {
321-
$exitStatus = EXIT_ERROR;
322-
}
320+
if ($exception instanceof HTTPExceptionInterface) {
321+
$statusCode = $exception->getCode();
322+
}
323323

324-
$statusCode = 500;
325-
} else {
326-
$exitStatus = EXIT_ERROR;
324+
if ($exception instanceof HasExitCodeInterface) {
325+
$exitStatus = $exception->getExitCode();
327326
}
328327

329328
return [$statusCode, $exitStatus];

system/Entity/Exceptions/CastException.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@
1212
namespace CodeIgniter\Entity\Exceptions;
1313

1414
use CodeIgniter\Exceptions\FrameworkException;
15+
use CodeIgniter\Exceptions\HasExitCodeInterface;
1516

1617
/**
1718
* CastException is thrown for invalid cast initialization and management.
18-
*
19-
* @TODO CodeIgniter\Exceptions\CastException is deprecated and this class is used.
20-
* CodeIgniter\Exceptions\CastException has the property $code = EXIT_CONFIG,
21-
* but this class does not have the code.
2219
*/
23-
class CastException extends FrameworkException
20+
class CastException extends FrameworkException implements HasExitCodeInterface
2421
{
22+
public function getExitCode(): int
23+
{
24+
return EXIT_CONFIG;
25+
}
26+
2527
/**
2628
* Thrown when the cast class does not extends BaseCast.
2729
*

system/Exceptions/CastException.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,14 @@
1818
*
1919
* @codeCoverageIgnore
2020
*/
21-
class CastException extends CriticalError
21+
class CastException extends CriticalError implements HasExitCodeInterface
2222
{
2323
use DebugTraceableTrait;
2424

25-
/**
26-
* Exit status code
27-
*
28-
* @var int
29-
*/
30-
protected $code = EXIT_CONFIG;
25+
public function getExitCode(): int
26+
{
27+
return EXIT_CONFIG;
28+
}
3129

3230
public static function forInvalidJsonFormatException(int $error)
3331
{

system/Exceptions/ConfigException.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,14 @@
1414
/**
1515
* Exception for automatic logging.
1616
*/
17-
class ConfigException extends CriticalError
17+
class ConfigException extends CriticalError implements HasExitCodeInterface
1818
{
1919
use DebugTraceableTrait;
2020

21-
/**
22-
* Exit status code
23-
*
24-
* @var int
25-
*/
26-
protected $code = EXIT_CONFIG;
21+
public function getExitCode(): int
22+
{
23+
return EXIT_CONFIG;
24+
}
2725

2826
public static function forDisabledMigrations()
2927
{
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <[email protected]>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\Exceptions;
13+
14+
/**
15+
* Interface for Exceptions that has exception code as HTTP status code.
16+
*/
17+
interface HTTPExceptionInterface
18+
{
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <[email protected]>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\Exceptions;
13+
14+
/**
15+
* Interface for Exceptions that has exception code as exit code.
16+
*/
17+
interface HasExitCodeInterface
18+
{
19+
/**
20+
* Returns exit status code.
21+
*/
22+
public function getExitCode(): int;
23+
}

system/Exceptions/PageNotFoundException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use Config\Services;
1515
use OutOfBoundsException;
1616

17-
class PageNotFoundException extends OutOfBoundsException implements ExceptionInterface
17+
class PageNotFoundException extends OutOfBoundsException implements ExceptionInterface, HTTPExceptionInterface
1818
{
1919
use DebugTraceableTrait;
2020

system/Router/Exceptions/RedirectException.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111

1212
namespace CodeIgniter\Router\Exceptions;
1313

14+
use CodeIgniter\Exceptions\HTTPExceptionInterface;
1415
use Exception;
1516

1617
/**
1718
* RedirectException
1819
*/
19-
class RedirectException extends Exception
20+
class RedirectException extends Exception implements HTTPExceptionInterface
2021
{
2122
/**
2223
* HTTP status code for redirects

tests/system/Debug/ExceptionsTest.php

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,13 @@ public function testDetermineCodes(): void
6060
{
6161
$determineCodes = $this->getPrivateMethodInvoker($this->exception, 'determineCodes');
6262

63-
$this->assertSame([500, EXIT__AUTO_MIN], $determineCodes(new RuntimeException('This.')));
63+
$this->assertSame([500, EXIT_ERROR], $determineCodes(new RuntimeException('This.')));
6464
$this->assertSame([500, EXIT_ERROR], $determineCodes(new RuntimeException('That.', 600)));
65-
$this->assertSame([404, EXIT_ERROR], $determineCodes(new RuntimeException('There.', 404)));
66-
$this->assertSame([167, EXIT_ERROR], $determineCodes(new RuntimeException('This.', 167)));
67-
// @TODO This exit code should be EXIT_CONFIG.
68-
$this->assertSame([500, 12], $determineCodes(new ConfigException('This.')));
69-
// @TODO This exit code should be EXIT_CONFIG.
70-
$this->assertSame([500, 9], $determineCodes(new CastException('This.')));
71-
// @TODO This exit code should be EXIT_DATABASE.
72-
$this->assertSame([500, 17], $determineCodes(new DatabaseException('This.')));
65+
$this->assertSame([500, EXIT_ERROR], $determineCodes(new RuntimeException('There.', 404)));
66+
$this->assertSame([500, EXIT_ERROR], $determineCodes(new RuntimeException('This.', 167)));
67+
$this->assertSame([500, EXIT_CONFIG], $determineCodes(new ConfigException('This.')));
68+
$this->assertSame([500, EXIT_CONFIG], $determineCodes(new CastException('This.')));
69+
$this->assertSame([500, EXIT_DATABASE], $determineCodes(new DatabaseException('This.')));
7370
}
7471

7572
public function testRenderBacktrace(): void

user_guide_src/source/changelogs/v4.3.0.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ Exceptions when Database Errors Occur
2323
- The exceptions thrown by the database connection classes have been changed to ``CodeIgniter\Database\Exceptions\DatabaseException``. Previously, different database drivers threw different exception classes, but these have been unified into ``DatabaseException``.
2424
- The exceptions thrown by the ``execute()`` method of Prepared Queries have been changed to ``DatabaseException``. Previously, different database drivers might throw different exception classes or did not throw exceptions, but these have been unified into ``DatabaseException``.
2525

26+
HTTP Status Code and Exit Code when Exception Occurs
27+
----------------------------------------------------
28+
29+
Previously, CodeIgniter's Exception Handler used the *Exception code* as the *HTTP status code* in some cases, and calculated the *Exit code* based on the Exception code. However there should be no logical connection with Exception code and HTTP Status Code or Exit code.
30+
31+
- Now the Exception Handler sets HTTP status code to ``500`` and set Exit code to the constant ``EXIT_ERROR`` (= ``1``) by default.
32+
- You can change HTTP status code or Exit code to implement ``HTTPExceptionInterface`` or ``HasExitCodeInterface`` in your Exception class. See :ref:`error-specify-http-status-code` and :ref:`error-specify-exit-code`.
33+
34+
For example, the Exit code has been changed like the following:
35+
36+
- If an uncaught ``ConfigException`` occurs, the Exit code is ``EXIT_CONFIG`` (= ``3``) instead of ``12``.
37+
- If an uncaught ``CastException`` occurs, the Exit code is ``EXIT_CONFIG`` (= ``3``) instead of ``9``.
38+
- If an uncaught ``DatabaseException`` occurs, the Exit code is ``EXIT_DATABASE`` (= ``8``) instead of ``17``.
39+
2640
Others
2741
------
2842

user_guide_src/source/general/errors.rst

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ To ignore logging on other status codes, you can set the status code to ignore i
6464
.. note:: It is possible that logging still will not happen for exceptions if your current Log settings
6565
are not set up to log **critical** errors, which all exceptions are logged as.
6666

67-
Custom Exceptions
68-
=================
67+
Framework Exceptions
68+
====================
6969

70-
The following custom exceptions are available:
70+
The following framework exceptions are available:
7171

7272
PageNotFoundException
7373
---------------------
@@ -89,7 +89,7 @@ is not the right type, etc:
8989

9090
.. literalinclude:: errors/008.php
9191

92-
This provides an HTTP status code of 500 and an exit code of 3.
92+
This provides an exit code of 3.
9393

9494
DatabaseException
9595
-----------------
@@ -99,7 +99,7 @@ or when it is temporarily lost:
9999

100100
.. literalinclude:: errors/009.php
101101

102-
This provides an HTTP status code of 500 and an exit code of 8.
102+
This provides an exit code of 8.
103103

104104
RedirectException
105105
-----------------
@@ -113,3 +113,23 @@ forcing a redirect to a specific route or URL:
113113
redirect code to use instead of the default (``302``, "temporary redirect"):
114114

115115
.. literalinclude:: errors/011.php
116+
117+
.. _error-specify-http-status-code:
118+
119+
Specify HTTP Status Code in Your Exception
120+
==========================================
121+
122+
Since v4.3.0, you can specify the HTTP status code for your Exception class to implement
123+
``HTTPExceptionInterface``.
124+
125+
When an exception implementing ``HTTPExceptionInterface`` is caught by CodeIgniter's exception handler, the Exception code will become the HTTP status code.
126+
127+
.. _error-specify-exit-code:
128+
129+
Specify Exit Code in Your Exception
130+
===================================
131+
132+
Since v4.3.0, you can specify the exit code for your Exception class to implement
133+
``HasExitCodeInterface``.
134+
135+
When an exception implementing ``HasExitCodeInterface`` is caught by CodeIgniter's exception handler, the code returned from the ``getExitCode()`` method will become the exit code.

user_guide_src/source/installation/upgrade_430.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ Add **types** to the properties in these Config classes. You may need to fix the
4444
Breaking Changes
4545
****************
4646

47+
HTTP Status Code and Exit Code of Uncaught Exceptions
48+
=====================================================
49+
50+
- If you expect *Exception code* as *HTTP status code*, the HTTP status code will be changed.
51+
In that case, you need to implement ``HTTPExceptionInterface`` in the Exception. See :ref:`error-specify-http-status-code`.
52+
- If you expect *Exit code* based on *Exception code*, the Exit code will be changed.
53+
In that case, you need to implement ``HasExitCodeInterface`` in the Exception. See :ref:`error-specify-exit-code`.
54+
55+
Others
56+
======
57+
4758
- The exception classes may be changed when database errors occur. If you catch the exceptions, you must confirm that your code can catch the exceptions. See :ref:`exceptions-when-database-errors-occur` for details.
4859

4960
Breaking Enhancements

0 commit comments

Comments
 (0)