Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HttpErrorCodeInterface. #18110

Merged
merged 1 commit into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add HttpErrorCodeInterface.
Using this interface allows exception classes to indicate that they
use HTTP error codes and avoid having to maintain an error class to HTTP error code
map in WebExceptionRenderer.
  • Loading branch information
ADmad committed Dec 29, 2024
commit d5d72dade319a9bdcdb93663896d5634ea55e78b
8 changes: 7 additions & 1 deletion src/Controller/Exception/InvalidParameterException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@
namespace Cake\Controller\Exception;

use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\HttpErrorCodeInterface;
use Throwable;

/**
* Used when a passed parameter or action parameter type declaration is missing or invalid.
*/
class InvalidParameterException extends CakeException
class InvalidParameterException extends CakeException implements HttpErrorCodeInterface
{
/**
* @inheritDoc
*/
protected int $_defaultCode = 404;

/**
* @var array<string, string>
*/
Expand Down
8 changes: 7 additions & 1 deletion src/Controller/Exception/MissingActionException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@
namespace Cake\Controller\Exception;

use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\HttpErrorCodeInterface;

/**
* Missing Action exception - used when a controller action
* cannot be found, or when the controller's isAction() method returns false.
*/
class MissingActionException extends CakeException
class MissingActionException extends CakeException implements HttpErrorCodeInterface
{
/**
* @inheritDoc
*/
protected int $_defaultCode = 404;

/**
* @inheritDoc
*/
Expand Down
21 changes: 21 additions & 0 deletions src/Core/Exception/HttpErrorCodeInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 5.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Core\Exception;

use Throwable;

interface HttpErrorCodeInterface extends Throwable
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this should have been in the Http package but couple of exceptions of the Datasource package are also mapped to 404 code :/

Copy link
Member

@othercorey othercorey Dec 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is ok. Hard to create a generic map with one way dependencies with our split packages.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we get much from extending Throwable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, I did it as this interface is only meant to be used by exception classes.

{
}
7 changes: 6 additions & 1 deletion src/Datasource/Exception/RecordNotFoundException.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@
namespace Cake\Datasource\Exception;

use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\HttpErrorCodeInterface;

/**
* Exception raised when a particular record was not found
*/
class RecordNotFoundException extends CakeException
class RecordNotFoundException extends CakeException implements HttpErrorCodeInterface
{
/**
* @inheritDoc
*/
protected int $_defaultCode = 404;
}
8 changes: 7 additions & 1 deletion src/Datasource/Paging/Exception/PageOutOfBoundsException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@
namespace Cake\Datasource\Paging\Exception;

use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\HttpErrorCodeInterface;

/**
* Exception raised when requested page number does not exist.
*/
class PageOutOfBoundsException extends CakeException
class PageOutOfBoundsException extends CakeException implements HttpErrorCodeInterface
{
/**
* @inheritDoc
*/
protected int $_defaultCode = 404;

/**
* @inheritDoc
*/
Expand Down
40 changes: 18 additions & 22 deletions src/Error/Renderer/WebExceptionRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,20 @@

use Cake\Controller\Controller;
use Cake\Controller\ControllerFactory;
use Cake\Controller\Exception\InvalidParameterException;
use Cake\Controller\Exception\MissingActionException;
use Cake\Core\App;
use Cake\Core\Configure;
use Cake\Core\Container;
use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\HttpErrorCodeInterface;
use Cake\Core\Exception\MissingPluginException;
use Cake\Datasource\Exception\RecordNotFoundException;
use Cake\Datasource\Paging\Exception\PageOutOfBoundsException;
use Cake\Error\Debugger;
use Cake\Error\ExceptionRendererInterface;
use Cake\Http\Exception\HttpException;
use Cake\Http\Exception\MissingControllerException;
use Cake\Http\Response;
use Cake\Http\ResponseEmitter;
use Cake\Http\ServerRequest;
use Cake\Http\ServerRequestFactory;
use Cake\Log\Log;
use Cake\Routing\Exception\MissingRouteException;
use Cake\Routing\Router;
use Cake\Utility\Inflector;
use Cake\View\Exception\MissingLayoutException;
Expand All @@ -45,6 +40,7 @@
use Psr\Http\Message\ResponseInterface;
use ReflectionMethod;
use Throwable;
use function Cake\Core\deprecationWarning;
use function Cake\Core\h;
use function Cake\Core\namespaceSplit;
use function Cake\I18n\__d;
Expand Down Expand Up @@ -110,21 +106,11 @@ class WebExceptionRenderer implements ExceptionRendererInterface
* This can be customized for users that don't want specific exceptions to throw 404 errors
* or want their application exceptions to be automatically converted.
*
* @var array<string, int>
* @psalm-var array<class-string<\Throwable>, int>
* @var array<class-string<\Throwable>, int>
* @deprecated 5.2.0 Exceptions returning HTTP error codes should extend
* HttpErrorCodeInterface instead of using this array.
*/
protected array $exceptionHttpCodes = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could end up breaking userland exception rendering with this change. If we wanted to avoid the breakage, we could retain this as an empty list and also retain line 408 as a fallback for exceptions that don't implement HttpErrorCodeInterface

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll add it back for BC.

// Controller exceptions
InvalidParameterException::class => 404,
MissingActionException::class => 404,
// Datasource exceptions
PageOutOfBoundsException::class => 404,
RecordNotFoundException::class => 404,
// Http exceptions
MissingControllerException::class => 404,
// Routing exceptions
MissingRouteException::class => 404,
];
protected array $exceptionHttpCodes = [];

/**
* Creates the controller to perform rendering on the error response.
Expand Down Expand Up @@ -401,11 +387,21 @@ protected function _template(Throwable $exception, string $method, int $code): s
*/
protected function getHttpCode(Throwable $exception): int
{
if ($exception instanceof HttpException) {
if ($exception instanceof HttpErrorCodeInterface) {
return $exception->getCode();
}

return $this->exceptionHttpCodes[$exception::class] ?? 500;
if (isset($this->exceptionHttpCodes[$exception::class])) {
deprecationWarning(
'5.2.0',
'Exceptions returning a HTTP error code should implement HttpErrorCodeInterface,'
. ' instead of using the WebExceptionRenderer::$exceptionHttpCodes property.'
);

return $this->exceptionHttpCodes[$exception::class];
}

return 500;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/Http/Exception/HttpException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Cake\Http\Exception;

use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\HttpErrorCodeInterface;

/**
* Parent class for all the HTTP related exceptions in CakePHP.
Expand All @@ -24,7 +25,7 @@
* You may also use this as a meaningful bridge to {@link \Cake\Core\Exception\CakeException}, e.g.:
* throw new \Cake\Network\Exception\HttpException('HTTP Version Not Supported', 505);
*/
class HttpException extends CakeException
class HttpException extends CakeException implements HttpErrorCodeInterface
{
/**
* @inheritDoc
Expand Down
3 changes: 2 additions & 1 deletion src/Http/Exception/MissingControllerException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
namespace Cake\Http\Exception;

use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\HttpErrorCodeInterface;

/**
* Missing Controller exception - used when a controller
* cannot be found.
*/
class MissingControllerException extends CakeException
class MissingControllerException extends CakeException implements HttpErrorCodeInterface
{
/**
* @inheritDoc
Expand Down
8 changes: 7 additions & 1 deletion src/Routing/Exception/MissingRouteException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,20 @@
namespace Cake\Routing\Exception;

use Cake\Core\Exception\CakeException;
use Cake\Core\Exception\HttpErrorCodeInterface;
use Throwable;

/**
* Exception raised when a URL cannot be reverse routed
* or when a URL cannot be parsed.
*/
class MissingRouteException extends CakeException
class MissingRouteException extends CakeException implements HttpErrorCodeInterface
{
/**
* @inheritDoc
*/
protected int $_defaultCode = 404;

/**
* @inheritDoc
*/
Expand Down
16 changes: 16 additions & 0 deletions tests/TestCase/Error/Renderer/WebExceptionRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
use OutOfBoundsException;
use PDOException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use ReflectionMethod;
use RuntimeException;
use TestApp\Controller\Admin\ErrorController as PrefixErrorController;
use TestApp\Error\Exception\MissingWidgetThing;
Expand Down Expand Up @@ -990,4 +992,18 @@ public function testExceptionWithMatchingControllerMethod(): void
$result = (string)$exceptionRenderer->render()->getBody();
$this->assertStringContainsString('<xml>rendered xml exception</xml>', $result);
}

#[WithoutErrorHandler]
public function testDeprecatedHttpErrorCodeMapping(): void
{
$this->deprecated(function () {
$exception = new MissingWidgetThing();
$exceptionRenderer = new MyCustomExceptionRenderer($exception);

$reflectedMethod = new ReflectionMethod($exceptionRenderer, 'getHttpCode');
$reflectedMethod->setAccessible(true);

$this->assertSame(404, $reflectedMethod->invoke($exceptionRenderer, $exception));
});
}
}
8 changes: 4 additions & 4 deletions tests/TestCase/ExceptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public static function exceptionProvider(): array
[MissingHelperException::class, 1],
[StopException::class, 1],
[FormProtectionException::class, 400],
[MissingActionException::class, 0],
[MissingActionException::class, 404],
[MissingComponentException::class, 0],
[CakeException::class, 0],
[MissingPluginException::class, 0],
Expand All @@ -195,8 +195,8 @@ public static function exceptionProvider(): array
[MissingDatasourceConfigException::class, 0],
[MissingDatasourceException::class, 0],
[MissingModelException::class, 0],
[RecordNotFoundException::class, 0],
[PageOutOfBoundsException::class, 0],
[RecordNotFoundException::class, 404],
[PageOutOfBoundsException::class, 404],
[MailerMissingActionException::class, 0],
[MissingMailerException::class, 0],
[BadRequestException::class, 400],
Expand All @@ -220,7 +220,7 @@ public static function exceptionProvider(): array
[MissingTableClassException::class, 0],
[RolledbackTransactionException::class, 0],
[DuplicateNamedRouteException::class, 0],
[MissingRouteException::class, 0],
[MissingRouteException::class, 404],
[XmlException::class, 0],
[MissingCellException::class, 0],
[ViewMissingHelperException::class, 0],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@

use Cake\Controller\Controller;
use Cake\Error\Renderer\WebExceptionRenderer;
use TestApp\Error\Exception\MissingWidgetThing;

class MyCustomExceptionRenderer extends WebExceptionRenderer
{
protected array $exceptionHttpCodes = [
MissingWidgetThing::class => 404,
];

/**
* @param \Cake\Controller\Controller $controller
*/
Expand Down
Loading