feat: custom exception handler - #7087

Merged
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4
Apr 9, 2023
Merged

feat: custom exception handler#7087
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4

Conversation

@kenjis

@kenjiskenjis commented Jan 11, 2023

Copy link
Copy Markdown
Member

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.

  • add ExceptionHandlerInterface, BaseExceptionHandler, and ExceptionHandler
  • add Config\Exceptions::handler() to define Exception handler

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@kenjiskenjis added the 4.4 label Jan 11, 2023
@kenjis
kenjis marked this pull request as draft January 11, 2023 07:57
@kenjiskenjis added the enhancement PRs that improve existing functionalities label Jan 11, 2023
@kenjis
kenjis changed the base branch from develop to 4.4January 11, 2023 07:58
@iRedds

Copy link
Copy Markdown
Collaborator

debug is debug. A custom exception handler is not a debug.
No need to wrap it in this namespace. It's about something else.

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 3 times, most recently from 4191539 to 179c3c2CompareFebruary 8, 2023 02:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

Reworked.

@kenjis
kenjis marked this pull request as ready for review February 8, 2023 04:02
@iRedds

Copy link
Copy Markdown
Collaborator

In addition to what has already been said, I will criticize the implementation.

  1. HTTP 404 should not be handled in exceptions. The handler already exists in the CodeIgniter class.
    If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

  2. I don't think it's correct that the Config\Exceptions::handler() method returns an ExceptionHandler instance by default.
    It's an easy way to shoot yourself in the foot. In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.
    It will also save 100500 changes to the application configs just because of adding functionality.

  3. Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.
    I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

  4. BaseExceptionHandler::render() method. According to the name, it should render the result, but not output it. And in general it can be replaced with view('template');

  5. The current implementation ignores logging.

  6. It seems to me that it would be more correct for exception handlers to return a Responce object.

@iRedds

Copy link
Copy Markdown
Collaborator

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from b4b9d31 to f988c93CompareFebruary 11, 2023 07:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501?
And is it relevant to this PR?

@kenjis

Copy link
Copy Markdown
MemberAuthor

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated.
So the existing apps do not need to define Config\Exceptions::handler().

@kenjis

Copy link
Copy Markdown
MemberAuthor

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The current implementation ignores logging.

You are correct. Thank you.
Fixed to log before calling ExceptionHandler::handle().

@kenjis

Copy link
Copy Markdown
MemberAuthor

It seems to me that it would be more correct for exception handlers to return a Responce object.

I'm not sure. But exception handlers might not return (HTTP) Response.

@iRedds

Copy link
Copy Markdown
Collaborator

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501? And is it relevant to this PR?

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.

@iRedds

Copy link
Copy Markdown
Collaborator

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated. So the existing apps do not need to define Config\Exceptions::handler().

Then can you explain why you need ExceptionHandler, if everything should work without it?
I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

publicfunctionhandler(int$statusCode, Throwable$exception): ExceptionHandlerInterface
{
returnnewExceptionHandler($this);
}

@iRedds

Copy link
Copy Markdown
Collaborator

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

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.
If I'm not mistaken, the Model::findOrFail() method, if the entry is not found, will throw an exception, which will be handled as 404 ending the program execution.

That is, you do not need checks in the controller.

@iRedds

Copy link
Copy Markdown
Collaborator

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

Then I have doubts about the need for this class.
It might be better to implement it as a trait.

@iRedds

Copy link
Copy Markdown
Collaborator

I'm not sure. But exception handlers might not return (HTTP) Response.

This is to avoid avoiding behavior like the CodeIgniter class, where $response->send() is called in many places instead of just one.
If we send the response from one place, it will be easier to control the behavior.

For example, this code is duplicated in CodeIgniter\Debug\Exceptions and CodeIgniter\Debug\ExceptionHandler

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);
}

@iRedds

Copy link
Copy Markdown
Collaborator

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

@kenjis

Copy link
Copy Markdown
MemberAuthor

Then can you explain why you need ExceptionHandler, if everything should work without it?

Everything works without it, because we keep BC.
The current code (which should be removed in the future) does everything as before.

I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

Yes.

@kenjis

Copy link
Copy Markdown
MemberAuthor

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

No, they are not real exceptions. But the framework already takes care of them.
What does this have to do with this PR?

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.

2xx is normal response, not an exception.

@kenjis

kenjis commented Feb 11, 2023

Copy link
Copy Markdown
MemberAuthor

If we send the response from one place, it will be easier to control the behavior.

I agree, but it seems not the scope of this PR.

We cannot solve many problems at once while keeping backward compatibility.
Please send PRs one at a time if necessary.
This PR allows for the definition of custom exception handlers.

@iRedds

Copy link
Copy Markdown
Collaborator

Everything works without it, because we keep BC. The current code (which should be removed in the future) does everything as before.

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 = newExceptionHandler();
}

Given that the ExceptionHandler class implements the current functionality, nothing will change.
BC will not be here, since the result will not change.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The following implementation is more breaking than this PR.
Because ExceptionHandler is a new class, and not the same as Exceptions.

if () { // trying to get a custom handler.// $handler = 
} else {
$handler = newExceptionHandler();
}

If a dev overrides Exceptions::render(), it breaks.

@iRedds

Copy link
Copy Markdown
Collaborator

Yes, redirects are normal responses, not exceptional. Most cases of 4xx are also expected, not exceptional.

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?

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.
However, deleting what is already there is not possible because it is a braking change.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from f988c93 to d9a1016CompareFebruary 13, 2023 23:49
@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 2 times, most recently from c119236 to 6d581d8CompareFebruary 25, 2023 03:29
@kenjiskenjis mentioned this pull request Feb 27, 2023
5 tasks
@MGatner

Copy link
Copy Markdown
Member

@kenjis can you summarize what changed from Lonnie's PR? Does this need a full re-review?

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6d581d8 to 6843cdfCompareMarch 2, 2023 01:06
@kenjis

kenjis commented Mar 2, 2023

Copy link
Copy Markdown
MemberAuthor

I honestly don't remember exactly, but I think #6710 was just an update of Lonnie's PR.
This PR is basically the same, but keeps more compatibility than that.

Changes:

The difference between #6710 and this (including the changes in 4.4 branch):

Details
$ 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 <admin@codeigniter.com>+ *+ * 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);
}

@kenjis

Copy link
Copy Markdown
MemberAuthor

Does this need a full re-review?

No. The code structure is not changed since the Lonnie's PR.
The review point is the upgrade guide is sufficient or not.
In other words, whether there are any breaking changes other than documented.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6843cdf to 7e03ff1CompareMarch 12, 2023 00:47
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready to merge.
@codeigniter4/core-team Can someone review?

@MGatner

Copy link
Copy Markdown
Member

I'll have to come back to this, it's a big one.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 7e03ff1 to 07a5194CompareApril 8, 2023 02:05
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.
Can someone review?

@kenjis
kenjis merged commit 95a6be9 into codeigniter4:4.4Apr 9, 2023
@kenjis
kenjis deleted the feat-ExceptionHandler-4.4 branch April 9, 2023 23:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changePull requests that may break existing functionalitiesenhancementPRs that improve existing functionalities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@kenjis@iRedds@MGatner@lonnieezell
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: custom exception handler - #7087

Merged
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4
Apr 9, 2023
Merged

feat: custom exception handler#7087
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4

Conversation

@kenjis

@kenjiskenjis commented Jan 11, 2023

Copy link
Copy Markdown
Member

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.

  • add ExceptionHandlerInterface, BaseExceptionHandler, and ExceptionHandler
  • add Config\Exceptions::handler() to define Exception handler

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@kenjiskenjis added the 4.4 label Jan 11, 2023
@kenjis
kenjis marked this pull request as draft January 11, 2023 07:57
@kenjiskenjis added the enhancement PRs that improve existing functionalities label Jan 11, 2023
@kenjis
kenjis changed the base branch from develop to 4.4January 11, 2023 07:58
@iRedds

Copy link
Copy Markdown
Collaborator

debug is debug. A custom exception handler is not a debug.
No need to wrap it in this namespace. It's about something else.

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 3 times, most recently from 4191539 to 179c3c2CompareFebruary 8, 2023 02:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

Reworked.

@kenjis
kenjis marked this pull request as ready for review February 8, 2023 04:02
@iRedds

Copy link
Copy Markdown
Collaborator

In addition to what has already been said, I will criticize the implementation.

  1. HTTP 404 should not be handled in exceptions. The handler already exists in the CodeIgniter class.
    If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

  2. I don't think it's correct that the Config\Exceptions::handler() method returns an ExceptionHandler instance by default.
    It's an easy way to shoot yourself in the foot. In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.
    It will also save 100500 changes to the application configs just because of adding functionality.

  3. Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.
    I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

  4. BaseExceptionHandler::render() method. According to the name, it should render the result, but not output it. And in general it can be replaced with view('template');

  5. The current implementation ignores logging.

  6. It seems to me that it would be more correct for exception handlers to return a Responce object.

@iRedds

Copy link
Copy Markdown
Collaborator

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from b4b9d31 to f988c93CompareFebruary 11, 2023 07:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501?
And is it relevant to this PR?

@kenjis

Copy link
Copy Markdown
MemberAuthor

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated.
So the existing apps do not need to define Config\Exceptions::handler().

@kenjis

Copy link
Copy Markdown
MemberAuthor

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The current implementation ignores logging.

You are correct. Thank you.
Fixed to log before calling ExceptionHandler::handle().

@kenjis

Copy link
Copy Markdown
MemberAuthor

It seems to me that it would be more correct for exception handlers to return a Responce object.

I'm not sure. But exception handlers might not return (HTTP) Response.

@iRedds

Copy link
Copy Markdown
Collaborator

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501? And is it relevant to this PR?

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.

@iRedds

Copy link
Copy Markdown
Collaborator

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated. So the existing apps do not need to define Config\Exceptions::handler().

Then can you explain why you need ExceptionHandler, if everything should work without it?
I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

publicfunctionhandler(int$statusCode, Throwable$exception): ExceptionHandlerInterface
{
returnnewExceptionHandler($this);
}

@iRedds

Copy link
Copy Markdown
Collaborator

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

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.
If I'm not mistaken, the Model::findOrFail() method, if the entry is not found, will throw an exception, which will be handled as 404 ending the program execution.

That is, you do not need checks in the controller.

@iRedds

Copy link
Copy Markdown
Collaborator

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

Then I have doubts about the need for this class.
It might be better to implement it as a trait.

@iRedds

Copy link
Copy Markdown
Collaborator

I'm not sure. But exception handlers might not return (HTTP) Response.

This is to avoid avoiding behavior like the CodeIgniter class, where $response->send() is called in many places instead of just one.
If we send the response from one place, it will be easier to control the behavior.

For example, this code is duplicated in CodeIgniter\Debug\Exceptions and CodeIgniter\Debug\ExceptionHandler

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);
}

@iRedds

Copy link
Copy Markdown
Collaborator

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

@kenjis

Copy link
Copy Markdown
MemberAuthor

Then can you explain why you need ExceptionHandler, if everything should work without it?

Everything works without it, because we keep BC.
The current code (which should be removed in the future) does everything as before.

I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

Yes.

@kenjis

Copy link
Copy Markdown
MemberAuthor

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

No, they are not real exceptions. But the framework already takes care of them.
What does this have to do with this PR?

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.

2xx is normal response, not an exception.

@kenjis

kenjis commented Feb 11, 2023

Copy link
Copy Markdown
MemberAuthor

If we send the response from one place, it will be easier to control the behavior.

I agree, but it seems not the scope of this PR.

We cannot solve many problems at once while keeping backward compatibility.
Please send PRs one at a time if necessary.
This PR allows for the definition of custom exception handlers.

@iRedds

Copy link
Copy Markdown
Collaborator

Everything works without it, because we keep BC. The current code (which should be removed in the future) does everything as before.

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 = newExceptionHandler();
}

Given that the ExceptionHandler class implements the current functionality, nothing will change.
BC will not be here, since the result will not change.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The following implementation is more breaking than this PR.
Because ExceptionHandler is a new class, and not the same as Exceptions.

if () { // trying to get a custom handler.// $handler = 
} else {
$handler = newExceptionHandler();
}

If a dev overrides Exceptions::render(), it breaks.

@iRedds

Copy link
Copy Markdown
Collaborator

Yes, redirects are normal responses, not exceptional. Most cases of 4xx are also expected, not exceptional.

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?

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.
However, deleting what is already there is not possible because it is a braking change.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from f988c93 to d9a1016CompareFebruary 13, 2023 23:49
@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 2 times, most recently from c119236 to 6d581d8CompareFebruary 25, 2023 03:29
@kenjiskenjis mentioned this pull request Feb 27, 2023
5 tasks
@MGatner

Copy link
Copy Markdown
Member

@kenjis can you summarize what changed from Lonnie's PR? Does this need a full re-review?

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6d581d8 to 6843cdfCompareMarch 2, 2023 01:06
@kenjis

kenjis commented Mar 2, 2023

Copy link
Copy Markdown
MemberAuthor

I honestly don't remember exactly, but I think #6710 was just an update of Lonnie's PR.
This PR is basically the same, but keeps more compatibility than that.

Changes:

The difference between #6710 and this (including the changes in 4.4 branch):

Details
$ 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 <admin@codeigniter.com>+ *+ * 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);
}

@kenjis

Copy link
Copy Markdown
MemberAuthor

Does this need a full re-review?

No. The code structure is not changed since the Lonnie's PR.
The review point is the upgrade guide is sufficient or not.
In other words, whether there are any breaking changes other than documented.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6843cdf to 7e03ff1CompareMarch 12, 2023 00:47
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready to merge.
@codeigniter4/core-team Can someone review?

@MGatner

Copy link
Copy Markdown
Member

I'll have to come back to this, it's a big one.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 7e03ff1 to 07a5194CompareApril 8, 2023 02:05
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.
Can someone review?

@kenjis
kenjis merged commit 95a6be9 into codeigniter4:4.4Apr 9, 2023
@kenjis
kenjis deleted the feat-ExceptionHandler-4.4 branch April 9, 2023 23:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changePull requests that may break existing functionalitiesenhancementPRs that improve existing functionalities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@kenjis@iRedds@MGatner@lonnieezell
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: custom exception handler - #7087

Merged
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4
Apr 9, 2023
Merged

feat: custom exception handler#7087
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4

Conversation

@kenjis

@kenjiskenjis commented Jan 11, 2023

Copy link
Copy Markdown
Member

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.

  • add ExceptionHandlerInterface, BaseExceptionHandler, and ExceptionHandler
  • add Config\Exceptions::handler() to define Exception handler

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@kenjiskenjis added the 4.4 label Jan 11, 2023
@kenjis
kenjis marked this pull request as draft January 11, 2023 07:57
@kenjiskenjis added the enhancement PRs that improve existing functionalities label Jan 11, 2023
@kenjis
kenjis changed the base branch from develop to 4.4January 11, 2023 07:58
@iRedds

Copy link
Copy Markdown
Collaborator

debug is debug. A custom exception handler is not a debug.
No need to wrap it in this namespace. It's about something else.

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 3 times, most recently from 4191539 to 179c3c2CompareFebruary 8, 2023 02:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

Reworked.

@kenjis
kenjis marked this pull request as ready for review February 8, 2023 04:02
@iRedds

Copy link
Copy Markdown
Collaborator

In addition to what has already been said, I will criticize the implementation.

  1. HTTP 404 should not be handled in exceptions. The handler already exists in the CodeIgniter class.
    If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

  2. I don't think it's correct that the Config\Exceptions::handler() method returns an ExceptionHandler instance by default.
    It's an easy way to shoot yourself in the foot. In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.
    It will also save 100500 changes to the application configs just because of adding functionality.

  3. Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.
    I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

  4. BaseExceptionHandler::render() method. According to the name, it should render the result, but not output it. And in general it can be replaced with view('template');

  5. The current implementation ignores logging.

  6. It seems to me that it would be more correct for exception handlers to return a Responce object.

@iRedds

Copy link
Copy Markdown
Collaborator

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from b4b9d31 to f988c93CompareFebruary 11, 2023 07:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501?
And is it relevant to this PR?

@kenjis

Copy link
Copy Markdown
MemberAuthor

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated.
So the existing apps do not need to define Config\Exceptions::handler().

@kenjis

Copy link
Copy Markdown
MemberAuthor

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The current implementation ignores logging.

You are correct. Thank you.
Fixed to log before calling ExceptionHandler::handle().

@kenjis

Copy link
Copy Markdown
MemberAuthor

It seems to me that it would be more correct for exception handlers to return a Responce object.

I'm not sure. But exception handlers might not return (HTTP) Response.

@iRedds

Copy link
Copy Markdown
Collaborator

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501? And is it relevant to this PR?

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.

@iRedds

Copy link
Copy Markdown
Collaborator

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated. So the existing apps do not need to define Config\Exceptions::handler().

Then can you explain why you need ExceptionHandler, if everything should work without it?
I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

publicfunctionhandler(int$statusCode, Throwable$exception): ExceptionHandlerInterface
{
returnnewExceptionHandler($this);
}

@iRedds

Copy link
Copy Markdown
Collaborator

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

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.
If I'm not mistaken, the Model::findOrFail() method, if the entry is not found, will throw an exception, which will be handled as 404 ending the program execution.

That is, you do not need checks in the controller.

@iRedds

Copy link
Copy Markdown
Collaborator

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

Then I have doubts about the need for this class.
It might be better to implement it as a trait.

@iRedds

Copy link
Copy Markdown
Collaborator

I'm not sure. But exception handlers might not return (HTTP) Response.

This is to avoid avoiding behavior like the CodeIgniter class, where $response->send() is called in many places instead of just one.
If we send the response from one place, it will be easier to control the behavior.

For example, this code is duplicated in CodeIgniter\Debug\Exceptions and CodeIgniter\Debug\ExceptionHandler

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);
}

@iRedds

Copy link
Copy Markdown
Collaborator

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

@kenjis

Copy link
Copy Markdown
MemberAuthor

Then can you explain why you need ExceptionHandler, if everything should work without it?

Everything works without it, because we keep BC.
The current code (which should be removed in the future) does everything as before.

I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

Yes.

@kenjis

Copy link
Copy Markdown
MemberAuthor

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

No, they are not real exceptions. But the framework already takes care of them.
What does this have to do with this PR?

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.

2xx is normal response, not an exception.

@kenjis

kenjis commented Feb 11, 2023

Copy link
Copy Markdown
MemberAuthor

If we send the response from one place, it will be easier to control the behavior.

I agree, but it seems not the scope of this PR.

We cannot solve many problems at once while keeping backward compatibility.
Please send PRs one at a time if necessary.
This PR allows for the definition of custom exception handlers.

@iRedds

Copy link
Copy Markdown
Collaborator

Everything works without it, because we keep BC. The current code (which should be removed in the future) does everything as before.

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 = newExceptionHandler();
}

Given that the ExceptionHandler class implements the current functionality, nothing will change.
BC will not be here, since the result will not change.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The following implementation is more breaking than this PR.
Because ExceptionHandler is a new class, and not the same as Exceptions.

if () { // trying to get a custom handler.// $handler = 
} else {
$handler = newExceptionHandler();
}

If a dev overrides Exceptions::render(), it breaks.

@iRedds

Copy link
Copy Markdown
Collaborator

Yes, redirects are normal responses, not exceptional. Most cases of 4xx are also expected, not exceptional.

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?

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.
However, deleting what is already there is not possible because it is a braking change.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from f988c93 to d9a1016CompareFebruary 13, 2023 23:49
@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 2 times, most recently from c119236 to 6d581d8CompareFebruary 25, 2023 03:29
@kenjiskenjis mentioned this pull request Feb 27, 2023
5 tasks
@MGatner

Copy link
Copy Markdown
Member

@kenjis can you summarize what changed from Lonnie's PR? Does this need a full re-review?

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6d581d8 to 6843cdfCompareMarch 2, 2023 01:06
@kenjis

kenjis commented Mar 2, 2023

Copy link
Copy Markdown
MemberAuthor

I honestly don't remember exactly, but I think #6710 was just an update of Lonnie's PR.
This PR is basically the same, but keeps more compatibility than that.

Changes:

The difference between #6710 and this (including the changes in 4.4 branch):

Details
$ 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 <admin@codeigniter.com>+ *+ * 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);
}

@kenjis

Copy link
Copy Markdown
MemberAuthor

Does this need a full re-review?

No. The code structure is not changed since the Lonnie's PR.
The review point is the upgrade guide is sufficient or not.
In other words, whether there are any breaking changes other than documented.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6843cdf to 7e03ff1CompareMarch 12, 2023 00:47
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready to merge.
@codeigniter4/core-team Can someone review?

@MGatner

Copy link
Copy Markdown
Member

I'll have to come back to this, it's a big one.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 7e03ff1 to 07a5194CompareApril 8, 2023 02:05
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.
Can someone review?

@kenjis
kenjis merged commit 95a6be9 into codeigniter4:4.4Apr 9, 2023
@kenjis
kenjis deleted the feat-ExceptionHandler-4.4 branch April 9, 2023 23:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changePull requests that may break existing functionalitiesenhancementPRs that improve existing functionalities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@kenjis@iRedds@MGatner@lonnieezell
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: custom exception handler - #7087

Merged
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4
Apr 9, 2023
Merged

feat: custom exception handler#7087
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4

Conversation

@kenjis

@kenjiskenjis commented Jan 11, 2023

Copy link
Copy Markdown
Member

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.

  • add ExceptionHandlerInterface, BaseExceptionHandler, and ExceptionHandler
  • add Config\Exceptions::handler() to define Exception handler

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@kenjiskenjis added the 4.4 label Jan 11, 2023
@kenjis
kenjis marked this pull request as draft January 11, 2023 07:57
@kenjiskenjis added the enhancement PRs that improve existing functionalities label Jan 11, 2023
@kenjis
kenjis changed the base branch from develop to 4.4January 11, 2023 07:58
@iRedds

Copy link
Copy Markdown
Collaborator

debug is debug. A custom exception handler is not a debug.
No need to wrap it in this namespace. It's about something else.

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 3 times, most recently from 4191539 to 179c3c2CompareFebruary 8, 2023 02:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

Reworked.

@kenjis
kenjis marked this pull request as ready for review February 8, 2023 04:02
@iRedds

Copy link
Copy Markdown
Collaborator

In addition to what has already been said, I will criticize the implementation.

  1. HTTP 404 should not be handled in exceptions. The handler already exists in the CodeIgniter class.
    If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

  2. I don't think it's correct that the Config\Exceptions::handler() method returns an ExceptionHandler instance by default.
    It's an easy way to shoot yourself in the foot. In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.
    It will also save 100500 changes to the application configs just because of adding functionality.

  3. Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.
    I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

  4. BaseExceptionHandler::render() method. According to the name, it should render the result, but not output it. And in general it can be replaced with view('template');

  5. The current implementation ignores logging.

  6. It seems to me that it would be more correct for exception handlers to return a Responce object.

@iRedds

Copy link
Copy Markdown
Collaborator

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from b4b9d31 to f988c93CompareFebruary 11, 2023 07:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501?
And is it relevant to this PR?

@kenjis

Copy link
Copy Markdown
MemberAuthor

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated.
So the existing apps do not need to define Config\Exceptions::handler().

@kenjis

Copy link
Copy Markdown
MemberAuthor

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The current implementation ignores logging.

You are correct. Thank you.
Fixed to log before calling ExceptionHandler::handle().

@kenjis

Copy link
Copy Markdown
MemberAuthor

It seems to me that it would be more correct for exception handlers to return a Responce object.

I'm not sure. But exception handlers might not return (HTTP) Response.

@iRedds

Copy link
Copy Markdown
Collaborator

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501? And is it relevant to this PR?

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.

@iRedds

Copy link
Copy Markdown
Collaborator

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated. So the existing apps do not need to define Config\Exceptions::handler().

Then can you explain why you need ExceptionHandler, if everything should work without it?
I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

publicfunctionhandler(int$statusCode, Throwable$exception): ExceptionHandlerInterface
{
returnnewExceptionHandler($this);
}

@iRedds

Copy link
Copy Markdown
Collaborator

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

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.
If I'm not mistaken, the Model::findOrFail() method, if the entry is not found, will throw an exception, which will be handled as 404 ending the program execution.

That is, you do not need checks in the controller.

@iRedds

Copy link
Copy Markdown
Collaborator

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

Then I have doubts about the need for this class.
It might be better to implement it as a trait.

@iRedds

Copy link
Copy Markdown
Collaborator

I'm not sure. But exception handlers might not return (HTTP) Response.

This is to avoid avoiding behavior like the CodeIgniter class, where $response->send() is called in many places instead of just one.
If we send the response from one place, it will be easier to control the behavior.

For example, this code is duplicated in CodeIgniter\Debug\Exceptions and CodeIgniter\Debug\ExceptionHandler

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);
}

@iRedds

Copy link
Copy Markdown
Collaborator

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

@kenjis

Copy link
Copy Markdown
MemberAuthor

Then can you explain why you need ExceptionHandler, if everything should work without it?

Everything works without it, because we keep BC.
The current code (which should be removed in the future) does everything as before.

I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

Yes.

@kenjis

Copy link
Copy Markdown
MemberAuthor

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

No, they are not real exceptions. But the framework already takes care of them.
What does this have to do with this PR?

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.

2xx is normal response, not an exception.

@kenjis

kenjis commented Feb 11, 2023

Copy link
Copy Markdown
MemberAuthor

If we send the response from one place, it will be easier to control the behavior.

I agree, but it seems not the scope of this PR.

We cannot solve many problems at once while keeping backward compatibility.
Please send PRs one at a time if necessary.
This PR allows for the definition of custom exception handlers.

@iRedds

Copy link
Copy Markdown
Collaborator

Everything works without it, because we keep BC. The current code (which should be removed in the future) does everything as before.

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 = newExceptionHandler();
}

Given that the ExceptionHandler class implements the current functionality, nothing will change.
BC will not be here, since the result will not change.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The following implementation is more breaking than this PR.
Because ExceptionHandler is a new class, and not the same as Exceptions.

if () { // trying to get a custom handler.// $handler = 
} else {
$handler = newExceptionHandler();
}

If a dev overrides Exceptions::render(), it breaks.

@iRedds

Copy link
Copy Markdown
Collaborator

Yes, redirects are normal responses, not exceptional. Most cases of 4xx are also expected, not exceptional.

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?

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.
However, deleting what is already there is not possible because it is a braking change.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from f988c93 to d9a1016CompareFebruary 13, 2023 23:49
@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 2 times, most recently from c119236 to 6d581d8CompareFebruary 25, 2023 03:29
@kenjiskenjis mentioned this pull request Feb 27, 2023
5 tasks
@MGatner

Copy link
Copy Markdown
Member

@kenjis can you summarize what changed from Lonnie's PR? Does this need a full re-review?

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6d581d8 to 6843cdfCompareMarch 2, 2023 01:06
@kenjis

kenjis commented Mar 2, 2023

Copy link
Copy Markdown
MemberAuthor

I honestly don't remember exactly, but I think #6710 was just an update of Lonnie's PR.
This PR is basically the same, but keeps more compatibility than that.

Changes:

The difference between #6710 and this (including the changes in 4.4 branch):

Details
$ 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 <admin@codeigniter.com>+ *+ * 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);
}

@kenjis

Copy link
Copy Markdown
MemberAuthor

Does this need a full re-review?

No. The code structure is not changed since the Lonnie's PR.
The review point is the upgrade guide is sufficient or not.
In other words, whether there are any breaking changes other than documented.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6843cdf to 7e03ff1CompareMarch 12, 2023 00:47
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready to merge.
@codeigniter4/core-team Can someone review?

@MGatner

Copy link
Copy Markdown
Member

I'll have to come back to this, it's a big one.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 7e03ff1 to 07a5194CompareApril 8, 2023 02:05
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.
Can someone review?

@kenjis
kenjis merged commit 95a6be9 into codeigniter4:4.4Apr 9, 2023
@kenjis
kenjis deleted the feat-ExceptionHandler-4.4 branch April 9, 2023 23:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changePull requests that may break existing functionalitiesenhancementPRs that improve existing functionalities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@kenjis@iRedds@MGatner@lonnieezell
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: custom exception handler - #7087

Merged
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4
Apr 9, 2023
Merged

feat: custom exception handler#7087
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4

Conversation

@kenjis

@kenjiskenjis commented Jan 11, 2023

Copy link
Copy Markdown
Member

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.

  • add ExceptionHandlerInterface, BaseExceptionHandler, and ExceptionHandler
  • add Config\Exceptions::handler() to define Exception handler

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@kenjiskenjis added the 4.4 label Jan 11, 2023
@kenjis
kenjis marked this pull request as draft January 11, 2023 07:57
@kenjiskenjis added the enhancement PRs that improve existing functionalities label Jan 11, 2023
@kenjis
kenjis changed the base branch from develop to 4.4January 11, 2023 07:58
@iRedds

Copy link
Copy Markdown
Collaborator

debug is debug. A custom exception handler is not a debug.
No need to wrap it in this namespace. It's about something else.

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 3 times, most recently from 4191539 to 179c3c2CompareFebruary 8, 2023 02:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

Reworked.

@kenjis
kenjis marked this pull request as ready for review February 8, 2023 04:02
@iRedds

Copy link
Copy Markdown
Collaborator

In addition to what has already been said, I will criticize the implementation.

  1. HTTP 404 should not be handled in exceptions. The handler already exists in the CodeIgniter class.
    If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

  2. I don't think it's correct that the Config\Exceptions::handler() method returns an ExceptionHandler instance by default.
    It's an easy way to shoot yourself in the foot. In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.
    It will also save 100500 changes to the application configs just because of adding functionality.

  3. Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.
    I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

  4. BaseExceptionHandler::render() method. According to the name, it should render the result, but not output it. And in general it can be replaced with view('template');

  5. The current implementation ignores logging.

  6. It seems to me that it would be more correct for exception handlers to return a Responce object.

@iRedds

Copy link
Copy Markdown
Collaborator

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from b4b9d31 to f988c93CompareFebruary 11, 2023 07:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501?
And is it relevant to this PR?

@kenjis

Copy link
Copy Markdown
MemberAuthor

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated.
So the existing apps do not need to define Config\Exceptions::handler().

@kenjis

Copy link
Copy Markdown
MemberAuthor

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The current implementation ignores logging.

You are correct. Thank you.
Fixed to log before calling ExceptionHandler::handle().

@kenjis

Copy link
Copy Markdown
MemberAuthor

It seems to me that it would be more correct for exception handlers to return a Responce object.

I'm not sure. But exception handlers might not return (HTTP) Response.

@iRedds

Copy link
Copy Markdown
Collaborator

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501? And is it relevant to this PR?

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.

@iRedds

Copy link
Copy Markdown
Collaborator

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated. So the existing apps do not need to define Config\Exceptions::handler().

Then can you explain why you need ExceptionHandler, if everything should work without it?
I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

publicfunctionhandler(int$statusCode, Throwable$exception): ExceptionHandlerInterface
{
returnnewExceptionHandler($this);
}

@iRedds

Copy link
Copy Markdown
Collaborator

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

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.
If I'm not mistaken, the Model::findOrFail() method, if the entry is not found, will throw an exception, which will be handled as 404 ending the program execution.

That is, you do not need checks in the controller.

@iRedds

Copy link
Copy Markdown
Collaborator

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

Then I have doubts about the need for this class.
It might be better to implement it as a trait.

@iRedds

Copy link
Copy Markdown
Collaborator

I'm not sure. But exception handlers might not return (HTTP) Response.

This is to avoid avoiding behavior like the CodeIgniter class, where $response->send() is called in many places instead of just one.
If we send the response from one place, it will be easier to control the behavior.

For example, this code is duplicated in CodeIgniter\Debug\Exceptions and CodeIgniter\Debug\ExceptionHandler

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);
}

@iRedds

Copy link
Copy Markdown
Collaborator

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

@kenjis

Copy link
Copy Markdown
MemberAuthor

Then can you explain why you need ExceptionHandler, if everything should work without it?

Everything works without it, because we keep BC.
The current code (which should be removed in the future) does everything as before.

I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

Yes.

@kenjis

Copy link
Copy Markdown
MemberAuthor

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

No, they are not real exceptions. But the framework already takes care of them.
What does this have to do with this PR?

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.

2xx is normal response, not an exception.

@kenjis

kenjis commented Feb 11, 2023

Copy link
Copy Markdown
MemberAuthor

If we send the response from one place, it will be easier to control the behavior.

I agree, but it seems not the scope of this PR.

We cannot solve many problems at once while keeping backward compatibility.
Please send PRs one at a time if necessary.
This PR allows for the definition of custom exception handlers.

@iRedds

Copy link
Copy Markdown
Collaborator

Everything works without it, because we keep BC. The current code (which should be removed in the future) does everything as before.

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 = newExceptionHandler();
}

Given that the ExceptionHandler class implements the current functionality, nothing will change.
BC will not be here, since the result will not change.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The following implementation is more breaking than this PR.
Because ExceptionHandler is a new class, and not the same as Exceptions.

if () { // trying to get a custom handler.// $handler = 
} else {
$handler = newExceptionHandler();
}

If a dev overrides Exceptions::render(), it breaks.

@iRedds

Copy link
Copy Markdown
Collaborator

Yes, redirects are normal responses, not exceptional. Most cases of 4xx are also expected, not exceptional.

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?

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.
However, deleting what is already there is not possible because it is a braking change.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from f988c93 to d9a1016CompareFebruary 13, 2023 23:49
@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 2 times, most recently from c119236 to 6d581d8CompareFebruary 25, 2023 03:29
@kenjiskenjis mentioned this pull request Feb 27, 2023
5 tasks
@MGatner

Copy link
Copy Markdown
Member

@kenjis can you summarize what changed from Lonnie's PR? Does this need a full re-review?

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6d581d8 to 6843cdfCompareMarch 2, 2023 01:06
@kenjis

kenjis commented Mar 2, 2023

Copy link
Copy Markdown
MemberAuthor

I honestly don't remember exactly, but I think #6710 was just an update of Lonnie's PR.
This PR is basically the same, but keeps more compatibility than that.

Changes:

The difference between #6710 and this (including the changes in 4.4 branch):

Details
$ 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 <admin@codeigniter.com>+ *+ * 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);
}

@kenjis

Copy link
Copy Markdown
MemberAuthor

Does this need a full re-review?

No. The code structure is not changed since the Lonnie's PR.
The review point is the upgrade guide is sufficient or not.
In other words, whether there are any breaking changes other than documented.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6843cdf to 7e03ff1CompareMarch 12, 2023 00:47
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready to merge.
@codeigniter4/core-team Can someone review?

@MGatner

Copy link
Copy Markdown
Member

I'll have to come back to this, it's a big one.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 7e03ff1 to 07a5194CompareApril 8, 2023 02:05
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.
Can someone review?

@kenjis
kenjis merged commit 95a6be9 into codeigniter4:4.4Apr 9, 2023
@kenjis
kenjis deleted the feat-ExceptionHandler-4.4 branch April 9, 2023 23:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changePull requests that may break existing functionalitiesenhancementPRs that improve existing functionalities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@kenjis@iRedds@MGatner@lonnieezell
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: custom exception handler - #7087

Merged
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4
Apr 9, 2023
Merged

feat: custom exception handler#7087
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4

Conversation

@kenjis

@kenjiskenjis commented Jan 11, 2023

Copy link
Copy Markdown
Member

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.

  • add ExceptionHandlerInterface, BaseExceptionHandler, and ExceptionHandler
  • add Config\Exceptions::handler() to define Exception handler

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@kenjiskenjis added the 4.4 label Jan 11, 2023
@kenjis
kenjis marked this pull request as draft January 11, 2023 07:57
@kenjiskenjis added the enhancement PRs that improve existing functionalities label Jan 11, 2023
@kenjis
kenjis changed the base branch from develop to 4.4January 11, 2023 07:58
@iRedds

Copy link
Copy Markdown
Collaborator

debug is debug. A custom exception handler is not a debug.
No need to wrap it in this namespace. It's about something else.

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 3 times, most recently from 4191539 to 179c3c2CompareFebruary 8, 2023 02:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

Reworked.

@kenjis
kenjis marked this pull request as ready for review February 8, 2023 04:02
@iRedds

Copy link
Copy Markdown
Collaborator

In addition to what has already been said, I will criticize the implementation.

  1. HTTP 404 should not be handled in exceptions. The handler already exists in the CodeIgniter class.
    If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

  2. I don't think it's correct that the Config\Exceptions::handler() method returns an ExceptionHandler instance by default.
    It's an easy way to shoot yourself in the foot. In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.
    It will also save 100500 changes to the application configs just because of adding functionality.

  3. Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.
    I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

  4. BaseExceptionHandler::render() method. According to the name, it should render the result, but not output it. And in general it can be replaced with view('template');

  5. The current implementation ignores logging.

  6. It seems to me that it would be more correct for exception handlers to return a Responce object.

@iRedds

Copy link
Copy Markdown
Collaborator

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from b4b9d31 to f988c93CompareFebruary 11, 2023 07:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501?
And is it relevant to this PR?

@kenjis

Copy link
Copy Markdown
MemberAuthor

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated.
So the existing apps do not need to define Config\Exceptions::handler().

@kenjis

Copy link
Copy Markdown
MemberAuthor

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The current implementation ignores logging.

You are correct. Thank you.
Fixed to log before calling ExceptionHandler::handle().

@kenjis

Copy link
Copy Markdown
MemberAuthor

It seems to me that it would be more correct for exception handlers to return a Responce object.

I'm not sure. But exception handlers might not return (HTTP) Response.

@iRedds

Copy link
Copy Markdown
Collaborator

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501? And is it relevant to this PR?

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.

@iRedds

Copy link
Copy Markdown
Collaborator

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated. So the existing apps do not need to define Config\Exceptions::handler().

Then can you explain why you need ExceptionHandler, if everything should work without it?
I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

publicfunctionhandler(int$statusCode, Throwable$exception): ExceptionHandlerInterface
{
returnnewExceptionHandler($this);
}

@iRedds

Copy link
Copy Markdown
Collaborator

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

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.
If I'm not mistaken, the Model::findOrFail() method, if the entry is not found, will throw an exception, which will be handled as 404 ending the program execution.

That is, you do not need checks in the controller.

@iRedds

Copy link
Copy Markdown
Collaborator

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

Then I have doubts about the need for this class.
It might be better to implement it as a trait.

@iRedds

Copy link
Copy Markdown
Collaborator

I'm not sure. But exception handlers might not return (HTTP) Response.

This is to avoid avoiding behavior like the CodeIgniter class, where $response->send() is called in many places instead of just one.
If we send the response from one place, it will be easier to control the behavior.

For example, this code is duplicated in CodeIgniter\Debug\Exceptions and CodeIgniter\Debug\ExceptionHandler

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);
}

@iRedds

Copy link
Copy Markdown
Collaborator

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

@kenjis

Copy link
Copy Markdown
MemberAuthor

Then can you explain why you need ExceptionHandler, if everything should work without it?

Everything works without it, because we keep BC.
The current code (which should be removed in the future) does everything as before.

I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

Yes.

@kenjis

Copy link
Copy Markdown
MemberAuthor

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

No, they are not real exceptions. But the framework already takes care of them.
What does this have to do with this PR?

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.

2xx is normal response, not an exception.

@kenjis

kenjis commented Feb 11, 2023

Copy link
Copy Markdown
MemberAuthor

If we send the response from one place, it will be easier to control the behavior.

I agree, but it seems not the scope of this PR.

We cannot solve many problems at once while keeping backward compatibility.
Please send PRs one at a time if necessary.
This PR allows for the definition of custom exception handlers.

@iRedds

Copy link
Copy Markdown
Collaborator

Everything works without it, because we keep BC. The current code (which should be removed in the future) does everything as before.

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 = newExceptionHandler();
}

Given that the ExceptionHandler class implements the current functionality, nothing will change.
BC will not be here, since the result will not change.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The following implementation is more breaking than this PR.
Because ExceptionHandler is a new class, and not the same as Exceptions.

if () { // trying to get a custom handler.// $handler = 
} else {
$handler = newExceptionHandler();
}

If a dev overrides Exceptions::render(), it breaks.

@iRedds

Copy link
Copy Markdown
Collaborator

Yes, redirects are normal responses, not exceptional. Most cases of 4xx are also expected, not exceptional.

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?

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.
However, deleting what is already there is not possible because it is a braking change.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from f988c93 to d9a1016CompareFebruary 13, 2023 23:49
@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 2 times, most recently from c119236 to 6d581d8CompareFebruary 25, 2023 03:29
@kenjiskenjis mentioned this pull request Feb 27, 2023
5 tasks
@MGatner

Copy link
Copy Markdown
Member

@kenjis can you summarize what changed from Lonnie's PR? Does this need a full re-review?

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6d581d8 to 6843cdfCompareMarch 2, 2023 01:06
@kenjis

kenjis commented Mar 2, 2023

Copy link
Copy Markdown
MemberAuthor

I honestly don't remember exactly, but I think #6710 was just an update of Lonnie's PR.
This PR is basically the same, but keeps more compatibility than that.

Changes:

The difference between #6710 and this (including the changes in 4.4 branch):

Details
$ 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 <admin@codeigniter.com>+ *+ * 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);
}

@kenjis

Copy link
Copy Markdown
MemberAuthor

Does this need a full re-review?

No. The code structure is not changed since the Lonnie's PR.
The review point is the upgrade guide is sufficient or not.
In other words, whether there are any breaking changes other than documented.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6843cdf to 7e03ff1CompareMarch 12, 2023 00:47
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready to merge.
@codeigniter4/core-team Can someone review?

@MGatner

Copy link
Copy Markdown
Member

I'll have to come back to this, it's a big one.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 7e03ff1 to 07a5194CompareApril 8, 2023 02:05
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.
Can someone review?

@kenjis
kenjis merged commit 95a6be9 into codeigniter4:4.4Apr 9, 2023
@kenjis
kenjis deleted the feat-ExceptionHandler-4.4 branch April 9, 2023 23:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changePull requests that may break existing functionalitiesenhancementPRs that improve existing functionalities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@kenjis@iRedds@MGatner@lonnieezell
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: custom exception handler - #7087

Merged
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4
Apr 9, 2023
Merged

feat: custom exception handler#7087
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4

Conversation

@kenjis

@kenjiskenjis commented Jan 11, 2023

Copy link
Copy Markdown
Member

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.

  • add ExceptionHandlerInterface, BaseExceptionHandler, and ExceptionHandler
  • add Config\Exceptions::handler() to define Exception handler

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@kenjiskenjis added the 4.4 label Jan 11, 2023
@kenjis
kenjis marked this pull request as draft January 11, 2023 07:57
@kenjiskenjis added the enhancement PRs that improve existing functionalities label Jan 11, 2023
@kenjis
kenjis changed the base branch from develop to 4.4January 11, 2023 07:58
@iRedds

Copy link
Copy Markdown
Collaborator

debug is debug. A custom exception handler is not a debug.
No need to wrap it in this namespace. It's about something else.

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 3 times, most recently from 4191539 to 179c3c2CompareFebruary 8, 2023 02:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

Reworked.

@kenjis
kenjis marked this pull request as ready for review February 8, 2023 04:02
@iRedds

Copy link
Copy Markdown
Collaborator

In addition to what has already been said, I will criticize the implementation.

  1. HTTP 404 should not be handled in exceptions. The handler already exists in the CodeIgniter class.
    If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

  2. I don't think it's correct that the Config\Exceptions::handler() method returns an ExceptionHandler instance by default.
    It's an easy way to shoot yourself in the foot. In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.
    It will also save 100500 changes to the application configs just because of adding functionality.

  3. Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.
    I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

  4. BaseExceptionHandler::render() method. According to the name, it should render the result, but not output it. And in general it can be replaced with view('template');

  5. The current implementation ignores logging.

  6. It seems to me that it would be more correct for exception handlers to return a Responce object.

@iRedds

Copy link
Copy Markdown
Collaborator

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from b4b9d31 to f988c93CompareFebruary 11, 2023 07:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501?
And is it relevant to this PR?

@kenjis

Copy link
Copy Markdown
MemberAuthor

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated.
So the existing apps do not need to define Config\Exceptions::handler().

@kenjis

Copy link
Copy Markdown
MemberAuthor

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The current implementation ignores logging.

You are correct. Thank you.
Fixed to log before calling ExceptionHandler::handle().

@kenjis

Copy link
Copy Markdown
MemberAuthor

It seems to me that it would be more correct for exception handlers to return a Responce object.

I'm not sure. But exception handlers might not return (HTTP) Response.

@iRedds

Copy link
Copy Markdown
Collaborator

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501? And is it relevant to this PR?

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.

@iRedds

Copy link
Copy Markdown
Collaborator

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated. So the existing apps do not need to define Config\Exceptions::handler().

Then can you explain why you need ExceptionHandler, if everything should work without it?
I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

publicfunctionhandler(int$statusCode, Throwable$exception): ExceptionHandlerInterface
{
returnnewExceptionHandler($this);
}

@iRedds

Copy link
Copy Markdown
Collaborator

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

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.
If I'm not mistaken, the Model::findOrFail() method, if the entry is not found, will throw an exception, which will be handled as 404 ending the program execution.

That is, you do not need checks in the controller.

@iRedds

Copy link
Copy Markdown
Collaborator

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

Then I have doubts about the need for this class.
It might be better to implement it as a trait.

@iRedds

Copy link
Copy Markdown
Collaborator

I'm not sure. But exception handlers might not return (HTTP) Response.

This is to avoid avoiding behavior like the CodeIgniter class, where $response->send() is called in many places instead of just one.
If we send the response from one place, it will be easier to control the behavior.

For example, this code is duplicated in CodeIgniter\Debug\Exceptions and CodeIgniter\Debug\ExceptionHandler

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);
}

@iRedds

Copy link
Copy Markdown
Collaborator

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

@kenjis

Copy link
Copy Markdown
MemberAuthor

Then can you explain why you need ExceptionHandler, if everything should work without it?

Everything works without it, because we keep BC.
The current code (which should be removed in the future) does everything as before.

I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

Yes.

@kenjis

Copy link
Copy Markdown
MemberAuthor

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

No, they are not real exceptions. But the framework already takes care of them.
What does this have to do with this PR?

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.

2xx is normal response, not an exception.

@kenjis

kenjis commented Feb 11, 2023

Copy link
Copy Markdown
MemberAuthor

If we send the response from one place, it will be easier to control the behavior.

I agree, but it seems not the scope of this PR.

We cannot solve many problems at once while keeping backward compatibility.
Please send PRs one at a time if necessary.
This PR allows for the definition of custom exception handlers.

@iRedds

Copy link
Copy Markdown
Collaborator

Everything works without it, because we keep BC. The current code (which should be removed in the future) does everything as before.

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 = newExceptionHandler();
}

Given that the ExceptionHandler class implements the current functionality, nothing will change.
BC will not be here, since the result will not change.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The following implementation is more breaking than this PR.
Because ExceptionHandler is a new class, and not the same as Exceptions.

if () { // trying to get a custom handler.// $handler = 
} else {
$handler = newExceptionHandler();
}

If a dev overrides Exceptions::render(), it breaks.

@iRedds

Copy link
Copy Markdown
Collaborator

Yes, redirects are normal responses, not exceptional. Most cases of 4xx are also expected, not exceptional.

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?

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.
However, deleting what is already there is not possible because it is a braking change.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from f988c93 to d9a1016CompareFebruary 13, 2023 23:49
@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 2 times, most recently from c119236 to 6d581d8CompareFebruary 25, 2023 03:29
@kenjiskenjis mentioned this pull request Feb 27, 2023
5 tasks
@MGatner

Copy link
Copy Markdown
Member

@kenjis can you summarize what changed from Lonnie's PR? Does this need a full re-review?

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6d581d8 to 6843cdfCompareMarch 2, 2023 01:06
@kenjis

kenjis commented Mar 2, 2023

Copy link
Copy Markdown
MemberAuthor

I honestly don't remember exactly, but I think #6710 was just an update of Lonnie's PR.
This PR is basically the same, but keeps more compatibility than that.

Changes:

The difference between #6710 and this (including the changes in 4.4 branch):

Details
$ 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 <admin@codeigniter.com>+ *+ * 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);
}

@kenjis

Copy link
Copy Markdown
MemberAuthor

Does this need a full re-review?

No. The code structure is not changed since the Lonnie's PR.
The review point is the upgrade guide is sufficient or not.
In other words, whether there are any breaking changes other than documented.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6843cdf to 7e03ff1CompareMarch 12, 2023 00:47
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready to merge.
@codeigniter4/core-team Can someone review?

@MGatner

Copy link
Copy Markdown
Member

I'll have to come back to this, it's a big one.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 7e03ff1 to 07a5194CompareApril 8, 2023 02:05
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.
Can someone review?

@kenjis
kenjis merged commit 95a6be9 into codeigniter4:4.4Apr 9, 2023
@kenjis
kenjis deleted the feat-ExceptionHandler-4.4 branch April 9, 2023 23:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changePull requests that may break existing functionalitiesenhancementPRs that improve existing functionalities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@kenjis@iRedds@MGatner@lonnieezell
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: custom exception handler - #7087

Merged
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4
Apr 9, 2023
Merged

feat: custom exception handler#7087
kenjis merged 10 commits into
codeigniter4:4.4from
kenjis:feat-ExceptionHandler-4.4

Conversation

@kenjis

@kenjiskenjis commented Jan 11, 2023

Copy link
Copy Markdown
Member

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.

  • add ExceptionHandlerInterface, BaseExceptionHandler, and ExceptionHandler
  • add Config\Exceptions::handler() to define Exception handler

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@kenjiskenjis added the 4.4 label Jan 11, 2023
@kenjis
kenjis marked this pull request as draft January 11, 2023 07:57
@kenjiskenjis added the enhancement PRs that improve existing functionalities label Jan 11, 2023
@kenjis
kenjis changed the base branch from develop to 4.4January 11, 2023 07:58
@iRedds

Copy link
Copy Markdown
Collaborator

debug is debug. A custom exception handler is not a debug.
No need to wrap it in this namespace. It's about something else.

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 3 times, most recently from 4191539 to 179c3c2CompareFebruary 8, 2023 02:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

Reworked.

@kenjis
kenjis marked this pull request as ready for review February 8, 2023 04:02
@iRedds

Copy link
Copy Markdown
Collaborator

In addition to what has already been said, I will criticize the implementation.

  1. HTTP 404 should not be handled in exceptions. The handler already exists in the CodeIgniter class.
    If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

  2. I don't think it's correct that the Config\Exceptions::handler() method returns an ExceptionHandler instance by default.
    It's an easy way to shoot yourself in the foot. In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.
    It will also save 100500 changes to the application configs just because of adding functionality.

  3. Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.
    I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

  4. BaseExceptionHandler::render() method. According to the name, it should render the result, but not output it. And in general it can be replaced with view('template');

  5. The current implementation ignores logging.

  6. It seems to me that it would be more correct for exception handlers to return a Responce object.

@iRedds

Copy link
Copy Markdown
Collaborator

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.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from b4b9d31 to f988c93CompareFebruary 11, 2023 07:54
@kenjis

Copy link
Copy Markdown
MemberAuthor

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501?
And is it relevant to this PR?

@kenjis

Copy link
Copy Markdown
MemberAuthor

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated.
So the existing apps do not need to define Config\Exceptions::handler().

@kenjis

Copy link
Copy Markdown
MemberAuthor

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The current implementation ignores logging.

You are correct. Thank you.
Fixed to log before calling ExceptionHandler::handle().

@kenjis

Copy link
Copy Markdown
MemberAuthor

It seems to me that it would be more correct for exception handlers to return a Responce object.

I'm not sure. But exception handlers might not return (HTTP) Response.

@iRedds

Copy link
Copy Markdown
Collaborator

If the route is defined, but no controller or method is found, it's not a 404, but a 503. As in any other case, when a class or method is not found.

Why 503? Isn't it 501? And is it relevant to this PR?

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.

@iRedds

Copy link
Copy Markdown
Collaborator

In my opinion, an instance of ExceptionHandler should be created if the Config\Exceptions::handler method is not defined or returns nothing.

I don't see why the ExceptionHandler should be instantiated if the Config\Exceptions::handler() is defined.

In this PR if the Config\Exceptions::handler() is not defined, the ExceptionHandler will not be instantiated. So the existing apps do not need to define Config\Exceptions::handler().

Then can you explain why you need ExceptionHandler, if everything should work without it?
I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

publicfunctionhandler(int$statusCode, Throwable$exception): ExceptionHandlerInterface
{
returnnewExceptionHandler($this);
}

@iRedds

Copy link
Copy Markdown
Collaborator

Custom exceptions are needed to form the correct response. That is, to return, for example, HTTP 200 instead of debugging data or a stub on the production server.

A 200 Response is not exceptional. So Exception Handler is not related.

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

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.
If I'm not mistaken, the Model::findOrFail() method, if the entry is not found, will throw an exception, which will be handled as 404 ending the program execution.

That is, you do not need checks in the controller.

@iRedds

Copy link
Copy Markdown
Collaborator

I want to say that in this case the methods from BaseExceptionHandler (by the way, maybe we will stop calling the classes Base) are not needed in the principle.

You don't need to use BaseExceptionHandler. It just provides some methods.

Then I have doubts about the need for this class.
It might be better to implement it as a trait.

@iRedds

Copy link
Copy Markdown
Collaborator

I'm not sure. But exception handlers might not return (HTTP) Response.

This is to avoid avoiding behavior like the CodeIgniter class, where $response->send() is called in many places instead of just one.
If we send the response from one place, it will be easier to control the behavior.

For example, this code is duplicated in CodeIgniter\Debug\Exceptions and CodeIgniter\Debug\ExceptionHandler

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);
}

@iRedds

Copy link
Copy Markdown
Collaborator

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

@kenjis

Copy link
Copy Markdown
MemberAuthor

Then can you explain why you need ExceptionHandler, if everything should work without it?

Everything works without it, because we keep BC.
The current code (which should be removed in the future) does everything as before.

I assumed this would be the default exception handler when no custom one is set.
Especially since you added it to the config.

Yes.

@kenjis

Copy link
Copy Markdown
MemberAuthor

You need to look deeper into a tool like exceptions.
Redirect and HTTP 404 are not exceptions either, but the framework handles exceptions that generate responses with the appropriate codes.

No, they are not real exceptions. But the framework already takes care of them.
What does this have to do with this PR?

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.

2xx is normal response, not an exception.

@kenjis

kenjis commented Feb 11, 2023

Copy link
Copy Markdown
MemberAuthor

If we send the response from one place, it will be easier to control the behavior.

I agree, but it seems not the scope of this PR.

We cannot solve many problems at once while keeping backward compatibility.
Please send PRs one at a time if necessary.
This PR allows for the definition of custom exception handlers.

@iRedds

Copy link
Copy Markdown
Collaborator

Everything works without it, because we keep BC. The current code (which should be removed in the future) does everything as before.

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 = newExceptionHandler();
}

Given that the ExceptionHandler class implements the current functionality, nothing will change.
BC will not be here, since the result will not change.

@kenjis

Copy link
Copy Markdown
MemberAuthor

The following implementation is more breaking than this PR.
Because ExceptionHandler is a new class, and not the same as Exceptions.

if () { // trying to get a custom handler.// $handler = 
} else {
$handler = newExceptionHandler();
}

If a dev overrides Exceptions::render(), it breaks.

@iRedds

Copy link
Copy Markdown
Collaborator

Yes, redirects are normal responses, not exceptional. Most cases of 4xx are also expected, not exceptional.

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?

@kenjis

Copy link
Copy Markdown
MemberAuthor

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.
However, deleting what is already there is not possible because it is a braking change.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from f988c93 to d9a1016CompareFebruary 13, 2023 23:49
@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch 2 times, most recently from c119236 to 6d581d8CompareFebruary 25, 2023 03:29
@kenjiskenjis mentioned this pull request Feb 27, 2023
5 tasks
@MGatner

Copy link
Copy Markdown
Member

@kenjis can you summarize what changed from Lonnie's PR? Does this need a full re-review?

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6d581d8 to 6843cdfCompareMarch 2, 2023 01:06
@kenjis

kenjis commented Mar 2, 2023

Copy link
Copy Markdown
MemberAuthor

I honestly don't remember exactly, but I think #6710 was just an update of Lonnie's PR.
This PR is basically the same, but keeps more compatibility than that.

Changes:

The difference between #6710 and this (including the changes in 4.4 branch):

Details
$ 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 <admin@codeigniter.com>+ *+ * 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);
}

@kenjis

Copy link
Copy Markdown
MemberAuthor

Does this need a full re-review?

No. The code structure is not changed since the Lonnie's PR.
The review point is the upgrade guide is sufficient or not.
In other words, whether there are any breaking changes other than documented.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 6843cdf to 7e03ff1CompareMarch 12, 2023 00:47
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.

@kenjis

Copy link
Copy Markdown
MemberAuthor

I think this PR is ready to merge.
@codeigniter4/core-team Can someone review?

@MGatner

Copy link
Copy Markdown
Member

I'll have to come back to this, it's a big one.

@kenjis
kenjisforce-pushed the feat-ExceptionHandler-4.4 branch from 7e03ff1 to 07a5194CompareApril 8, 2023 02:05
@kenjis

Copy link
Copy Markdown
MemberAuthor

Rebased to resolve conflict.
Can someone review?

@kenjis
kenjis merged commit 95a6be9 into codeigniter4:4.4Apr 9, 2023
@kenjis
kenjis deleted the feat-ExceptionHandler-4.4 branch April 9, 2023 23:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changePull requests that may break existing functionalitiesenhancementPRs that improve existing functionalities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@kenjis@iRedds@MGatner@lonnieezell