-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: custom exception handler #7087
Conversation
debug is debug. A custom exception handler is not a debug. |
That may indeed be true. I have rebased the previous PR to make it mergeable. However, I don't remember what's in it anymore and need to remember what I was trying to do before. |
4191539
to
179c3c2
Compare
Reworked. |
In addition to what has already been said, I will criticize the implementation.
|
And by the way, when I suggested my implementation of a custom handler, @lonnieezell said that it's not good to override the behavior of framework exceptions, such as PageNotFoundException. |
b4b9d31
to
f988c93
Compare
Why 503? Isn't it 501? |
I don't see why the In this PR if the |
A 200 Response is not exceptional. So Exception Handler is not related. |
You don't need to use BaseExceptionHandler. It just provides some methods. |
You are correct. Thank you. |
I'm not sure. But exception handlers might not return (HTTP) Response. |
This was for an example. That is, in my opinion, if there is no controller or method, it should not return 404. This does not apply to PR. I just had to say it, because for some reason PageNotFoundException is first caught in its handler, and then thrown out again, but processed already in debug. |
Then can you explain why you need public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface
{
return new ExceptionHandler($this);
} |
You need to look deeper into a tool like exceptions. For example, I want to do a centralized processing of my exception and I want the answer to be exactly 200. Or 201 or some other response code for me. Let me take Laravel as an example. That is, you do not need checks in the controller. |
Then I have doubts about the need for this class. |
This is to avoid avoiding behavior like the For example, this code is duplicated in CodeIgniter\Debug\Exceptions and CodeIgniter\Debug\ExceptionHandler CodeIgniter4/system/Debug/Exceptions.php Lines 154 to 170 in d2d65cd
|
Also I would like this PR to (if possible) add the ability to use exceptions without registration. As I suggested on the forum. https://forum.codeigniter.com/showthread.php?tid=86567 |
Everything works without it, because we keep BC.
Yes. |
No, they are not real exceptions. But the framework already takes care of them.
2xx is normal response, not an exception. |
I agree, but it seems not the scope of this PR. We cannot solve many problems at once while keeping backward compatibility. |
Then I do not understand what prevents us from using the default exception handler right now. Something like this if () { // trying to get a custom handler.
// $handler =
} else {
$handler = new ExceptionHandler();
} Given that the ExceptionHandler class implements the current functionality, nothing will change. |
The following implementation is more breaking than this PR. if () { // trying to get a custom handler.
// $handler =
} else {
$handler = new ExceptionHandler();
} If a dev overrides |
If 3xx and 4xx are the same normal responses as 2xx, then why can the first ones be processed through exceptions (and 4xx is processed twice), but 2xx cannot? |
It is not an exceptional process, so we should not use exceptions. It would be cleaner to remove them all. |
f988c93
to
d9a1016
Compare
c119236
to
6d581d8
Compare
@kenjis can you summarize what changed from Lonnie's PR? Does this need a full re-review? |
6d581d8
to
6843cdf
Compare
I honestly don't remember exactly, but I think #6710 was just an update of Lonnie's PR. Changes:
The difference between #6710 and this (including the changes in 4.4 branch): $ git diff feat-ExceptionHandler..HEAD system/Debug/
diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php
index 5428f38994..1eaa05acee 100644
--- a/system/Debug/ExceptionHandler.php
+++ b/system/Debug/ExceptionHandler.php
@@ -20,7 +20,7 @@ use CodeIgniter\HTTP\ResponseInterface;
use Config\Paths;
use Throwable;
-final class ExceptionHandler extends BaseExceptionHandler
+final class ExceptionHandler extends BaseExceptionHandler implements ExceptionHandlerInterface
{
use ResponseTrait;
@@ -36,8 +36,6 @@ final class ExceptionHandler extends BaseExceptionHandler
/**
* Determines the correct way to display the error.
- *
- * @return void
*/
public function handle(
Throwable $exception,
@@ -45,7 +43,7 @@ final class ExceptionHandler extends BaseExceptionHandler
ResponseInterface $response,
int $statusCode,
int $exitCode
- ) {
+ ): void {
// ResponseTrait needs these properties.
$this->request = $request;
$this->response = $response;
diff --git a/system/Debug/ExceptionHandlerInterface.php b/system/Debug/ExceptionHandlerInterface.php
new file mode 100644
index 0000000000..bbfcb6ba70
--- /dev/null
+++ b/system/Debug/ExceptionHandlerInterface.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * This file is part of CodeIgniter 4 framework.
+ *
+ * (c) CodeIgniter Foundation <[email protected]>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Debug;
+
+use CodeIgniter\HTTP\RequestInterface;
+use CodeIgniter\HTTP\ResponseInterface;
+use Throwable;
+
+interface ExceptionHandlerInterface
+{
+ /**
+ * Determines the correct way to display the error.
+ */
+ public function handle(
+ Throwable $exception,
+ RequestInterface $request,
+ ResponseInterface $response,
+ int $statusCode,
+ int $exitCode
+ ): void;
+}
diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php
index 0479e64154..f77e313e4d 100644
--- a/system/Debug/Exceptions.php
+++ b/system/Debug/Exceptions.php
@@ -15,10 +15,12 @@ use CodeIgniter\API\ResponseTrait;
use CodeIgniter\Exceptions\HasExitCodeInterface;
use CodeIgniter\Exceptions\HTTPExceptionInterface;
use CodeIgniter\Exceptions\PageNotFoundException;
+use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Exceptions as ExceptionsConfig;
use Config\Paths;
+use Config\Services;
use ErrorException;
use Psr\Log\LogLevel;
use Throwable;
@@ -59,7 +61,7 @@ class Exceptions
/**
* The request.
*
- * @var RequestInterface
+ * @var RequestInterface|null
*/
protected $request;
@@ -70,25 +72,29 @@ class Exceptions
*/
protected $response;
+ private ?Throwable $exceptionCaughtByExceptionHandler = null;
+
/**
- * @param RequestInterface $request
+ * @param RequestInterface|null $request
+ *
+ * @deprecated The parameter $request and $response are deprecated. No longer used.
*/
- public function __construct(ExceptionsConfig $config, $request, ResponseInterface $response)
+ public function __construct(ExceptionsConfig $config, $request, ResponseInterface $response) /** @phpstan-ignore-line */
{
// For backward compatibility
$this->ob_level = ob_get_level();
$this->viewPath = rtrim($config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR;
- $this->config = $config;
- $this->request = $request;
- $this->response = $response;
+ $this->config = $config;
// workaround for upgraded users
+ // This causes "Deprecated: Creation of dynamic property" in PHP 8.2.
+ // @TODO remove this after dropping PHP 8.1 support.
if (! isset($this->config->sensitiveDataInTrace)) {
$this->config->sensitiveDataInTrace = [];
}
- if (! isset($this->config->logDeprecationsOnly, $this->config->deprecationLogLevel)) {
- $this->config->logDeprecationsOnly = false;
+ if (! isset($this->config->logDeprecations, $this->config->deprecationLogLevel)) {
+ $this->config->logDeprecations = false;
$this->config->deprecationLogLevel = LogLevel::WARNING;
}
}
@@ -113,6 +119,8 @@ class Exceptions
*/
public function exceptionHandler(Throwable $exception)
{
+ $this->exceptionCaughtByExceptionHandler = $exception;
+
[$statusCode, $exitCode] = $this->determineCodes($exception);
if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) {
@@ -124,14 +132,47 @@ class Exceptions
]);
}
- // For upgraded users who did not update the config file.
- if (! method_exists($this->config, 'handler')) {
- $handler = new ExceptionHandler($this->config);
- } else {
+ $this->request = Services::request();
+ $this->response = Services::response();
+
+ if (method_exists($this->config, 'handler')) {
+ // Use new ExceptionHandler
$handler = $this->config->handler($statusCode, $exception);
+ $handler->handle(
+ $exception,
+ $this->request,
+ $this->response,
+ $statusCode,
+ $exitCode
+ );
+
+ return;
}
- $handler->handle($exception, $this->request, $this->response, $statusCode, $exitCode);
+ // For backward compatibility
+ if (! is_cli()) {
+ try {
+ $this->response->setStatusCode($statusCode);
+ } catch (HTTPException $e) {
+ // Workaround for invalid HTTP status code.
+ $statusCode = 500;
+ $this->response->setStatusCode($statusCode);
+ }
+
+ if (! headers_sent()) {
+ header(sprintf('HTTP/%s %s %s', $this->request->getProtocolVersion(), $this->response->getStatusCode(), $this->response->getReasonPhrase()), true, $statusCode);
+ }
+
+ if (strpos($this->request->getHeaderLine('accept'), 'text/html') === false) {
+ $this->respond(ENVIRONMENT === 'development' ? $this->collectVars($exception, $statusCode) : '', $statusCode)->send();
+
+ exit($exitCode);
+ }
+ }
+
+ $this->render($exception, $statusCode);
+
+ exit($exitCode);
}
/**
@@ -145,7 +186,11 @@ class Exceptions
*/
public function errorHandler(int $severity, string $message, ?string $file = null, ?int $line = null)
{
- if ($this->isDeprecationError($severity) && $this->config->logDeprecationsOnly) {
+ if ($this->isDeprecationError($severity)) {
+ if (! $this->config->logDeprecations || (bool) env('CODEIGNITER_SCREAM_DEPRECATIONS')) {
+ throw new ErrorException($message, 0, $severity, $file, $line);
+ }
+
return $this->handleDeprecationError($message, $file, $line);
}
@@ -172,6 +217,13 @@ class Exceptions
['type' => $type, 'message' => $message, 'file' => $file, 'line' => $line] = $error;
+ if ($this->exceptionCaughtByExceptionHandler) {
+ $message .= "\n【Previous Exception】\n"
+ . get_class($this->exceptionCaughtByExceptionHandler) . "\n"
+ . $this->exceptionCaughtByExceptionHandler->getMessage() . "\n"
+ . $this->exceptionCaughtByExceptionHandler->getTraceAsString();
+ }
+
if (in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE], true)) {
$this->exceptionHandler(new ErrorException($message, 0, $type, $file, $line));
}
@@ -338,8 +390,6 @@ class Exceptions
}
/**
- * @noRector \Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector
- *
* @return true
*/
private function handleDeprecationError(string $message, ?string $file = null, ?int $line = null): bool
@@ -515,9 +565,6 @@ class Exceptions
case is_resource($value):
return sprintf('resource (%s)', get_resource_type($value));
- case is_string($value):
- return var_export(clean_path($value), true);
-
default:
return var_export($value, true);
} |
No. The code structure is not changed since the Lonnie's PR. |
6843cdf
to
7e03ff1
Compare
Rebased to resolve conflict. |
I think this PR is ready to merge. |
I'll have to come back to this, it's a big one. |
7e03ff1
to
07a5194
Compare
Rebased to resolve conflict. |
Description
Supersedes #5675, #6710
Allows developers to easily create and integrate their own Exception handler classes,
and limiting their usage to only the exception or HTTP status code desired.
ExceptionHandlerInterface
,BaseExceptionHandler
, andExceptionHandler
Config\Exceptions::handler()
to define Exception handlerChecklist: