snicco / psr7-error-handler
A powerful and customizable error handler for psr7 apps.
Requires
- php: ^7.4|^8.0
- psr/http-factory: ^1.0.0
- psr/http-message: ^1.0.0
- psr/log: ^1.1.1
Requires (Dev)
- nyholm/psr7: ^1.0.0
- phpunit/phpunit: ^9.5.13
Conflicts
- snicco/better-wp-api: <2.0.0-beta.9
- snicco/better-wp-cache: <2.0.0-beta.9
- snicco/better-wp-cache-bundle: <2.0.0-beta.9
- snicco/better-wp-cli: <2.0.0-beta.9
- snicco/better-wp-cli-testing: <2.0.0-beta.9
- snicco/better-wp-hooks: <2.0.0-beta.9
- snicco/better-wp-hooks-bundle: <2.0.0-beta.9
- snicco/better-wp-mail: <2.0.0-beta.9
- snicco/better-wp-mail-bundle: <2.0.0-beta.9
- snicco/better-wp-mail-testing: <2.0.0-beta.9
- snicco/better-wpdb: <2.0.0-beta.9
- snicco/better-wpdb-bundle: <2.0.0-beta.9
- snicco/blade-bridge: <2.0.0-beta.9
- snicco/blade-bundle: <2.0.0-beta.9
- snicco/content-negotiation-middleware: <2.0.0-beta.9
- snicco/debug-bundle: <2.0.0-beta.9
- snicco/default-headers-middleware: <2.0.0-beta.9
- snicco/eloquent: <2.0.0-beta.9
- snicco/encryption-bundle: <2.0.0-beta.9
- snicco/event-dispatcher: <2.0.0-beta.9
- snicco/event-dispatcher-testing: <2.0.0-beta.9
- snicco/guests-only-middleware: <1.0.0
- snicco/http-routing: <2.0.0-beta.9
- snicco/http-routing-bundle: <2.0.0-beta.9
- snicco/http-routing-testing: <2.0.0-beta.9
- snicco/https-only-middleware: <2.0.0-beta.9
- snicco/illuminate-container-bridge: <2.0.0-beta.9
- snicco/kernel: <2.0.0-beta.9
- snicco/kernel-testing: <2.0.0-beta.9
- snicco/method-override-middleware: <2.0.0-beta.9
- snicco/minimal-logger: <2.0.0-beta.9
- snicco/must-match-route-middleware: <2.0.0-beta.9
- snicco/no-robots-middleware: <2.0.0-beta.9
- snicco/open-redirect-protection-middleware: <2.0.0-beta.9
- snicco/payload-middleware: <2.0.0-beta.9
- snicco/pimple-bridge: <2.0.0-beta.9
- snicco/redirect-middleware: <2.0.0-beta.9
- snicco/session: <2.0.0-beta.9
- snicco/session-bundle: <2.0.0-beta.9
- snicco/session-psr16-bridge: <2.0.0-beta.9
- snicco/session-testing: <2.0.0-beta.9
- snicco/session-wp-bridge: <2.0.0-beta.9
- snicco/share-cookies-middleware: <2.0.0-beta.9
- snicco/signed-url: <2.0.0-beta.9
- snicco/signed-url-psr15-bridge: <2.0.0-beta.9
- snicco/signed-url-psr16-bridge: <2.0.0-beta.9
- snicco/signed-url-testing: <2.0.0-beta.9
- snicco/signed-url-wp-bridge: <2.0.0-beta.9
- snicco/str-arr: <2.0.0-beta.9
- snicco/templating: <2.0.0-beta.9
- snicco/templating-bundle: <2.0.0-beta.9
- snicco/testable-clock: <2.0.0-beta.9
- snicco/testing-bundle: <2.0.0-beta.9
- snicco/trailing-slash-middleware: <2.0.0-beta.9
- snicco/wp-auth-only-middleware: <2.0.0-beta.9
- snicco/wp-capability-middleware: <2.0.0-beta.9
- snicco/wp-capapility-middleware: <1.0.0
- snicco/wp-guests-only-middleware: <2.0.0-beta.9
- snicco/wp-nonce-middleware: <2.0.0-beta.9
- dev-master
- v2.0.0-beta.9
- v2.0.0-beta.8
- v2.0.0-beta.7
- v2.0.0-beta.6
- v2.0.0-beta.5
- v2.0.0-beta.4
- v2.0.0-beta.3
- v2.0.0-beta.2
- v2.0.0-beta.1
- v1.10.1
- v1.10.0
- v1.9.1
- v1.9.0
- v1.8.1
- v1.8.0
- v1.7.0
- v1.6.2
- v1.6.1
- v1.6.0
- v1.5.0
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.0
- v1.2.1
- v1.2.0
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.2
- v1.0.1
- v1.0.0
- dev-beta
This package is auto-updated.
Last update: 2024-12-07 15:13:24 UTC
README
The ErrorHandler component of the Snicco project is a standalone error handler for PHP applications that use PSR-7 requests.
Table of contents
- Installation
- Needed collaborators
- Full example
- Exception utilities
- Contributing
- Issues and PR's
- Security
Installation
composer require snicco/psr7-error-handler
Collaborators for the HTTP error handler
On a high level, the HttpErrorHandler
interface is responsible for transforming instances
of Throwable
into instances of Psr\Http\Message\ResponseInterface
.
This package provides a TestErrorHandler
, which just re-throws exceptions and
a ProductionErrorHandler
, which is the main focus of this documentation.
To instantiate the ProductionErrorHandler
, we need the following collaborators:
The RequestAwareLogger
The RequestAwareLogger
is a simple wrapper class around a PSR-3 logger.
It allows you to add log context to each log entry depending on the caught exception, the current request, etc.
The last argument passed to RequestAwareLogger::__construct()
is variadic and accepts instances
of RequestLogContext
.
This is how you use it:
(Monolog is just an example, you can use any PSR-3 logger.)
use Psr\Log\LogLevel; use Snicco\Component\Psr7ErrorHandler\Information\ExceptionInformation; use Snicco\Component\Psr7ErrorHandler\Log\RequestAwareLogger; use Snicco\Component\Psr7ErrorHandler\Log\RequestLogContext; $monolog = new Monolog\Logger(); $request_aware_logger = new RequestAwareLogger($monolog); // With custom exception levels. // By default, any status code > 500 will be LogLevel::CRITICAL // Anything below will be LogLevel::ERROR $request_aware_logger = new RequestAwareLogger($monolog, [ Throwable::class => LogLevel::ALERT, MyCustomException::class => LogLevel::WARNING ]) // With custom log context: class AddIPAddressFor403Exception implements RequestLogContext { public function add(array $context, ExceptionInformation $information) : array{ if(403 === $information->statusCode()) { $context['ip'] = $information->serverRequest()->getServerParams()['REMOTE_ADDR']; } return $context; } } // The last argument is variadic. $request_aware_logger = new RequestAwareLogger($monolog, [], new AddIPAddressFor403Exception());
The ExceptionInformationProvider
The ExceptionInformationProvider
is responsible for converting
instances of Throwable
to an instance of ExceptionInformation
.
ExceptionInformation
is a value object that consist of:
- a unique identifier for the exception that will be passed as log context and can be displayed to users.
- an HTTP status code that should be used when displaying the exception.
- a safe title for displaying the exception (safe meaning "does not contain sensitive information").
- a safe message for displaying the exception (safe meaning "does not contain sensitive information").
- the original
Throwable
- a transformed
Throwable
- the original instance of
Psr\Http\Message\ServerRequestInterface
This package comes with
a InformationProviderWithTransformation
implementation of
that interface.
You can instantiate this class like so:
use Snicco\Component\Psr7ErrorHandler\Identifier\SplHashIdentifier; use Snicco\Component\Psr7ErrorHandler\Information\InformationProviderWithTransformation; // uses spl_object_hash to uniquely identify exceptions. $identifier = new SplHashIdentifier(); // This will use the error messages in /resources/en_US.error.json $information_provider = InformationProviderWithTransformation::fromDefaultData($identifier); // Or with custom error messages $error_messages = [ // The 500 status code is mandatory. All other HTTP status codes are optional. 500 => [ 'title' => 'Whoops, this did not work...', 'message' => 'An error has occurred... We are sorry.' ]; ] $information_provider = new InformationProviderWithTransformation($error_messages, $identifier);
As its class name suggests,
the InformationProviderWithTransformation
allows you to transform exceptions into other kinds of exceptions.
This is done by using ExceptionTransformers
.
An example on how to transform custom exception classes to an instance of HttpException
.
use Snicco\Component\Psr7ErrorHandler\Information\ExceptionTransformer; class CustomAuthenticationTo404Transformer implements ExceptionTransformer { public function transform(Throwable $e) : Throwable{ if(!$e instanceof MyCustomAuthenticationException) { return $e; } // Key value pairs of headers that will later be added to the PSR-7 response. $response_headers = [ 'WWW-Authenticate' => '/login' ]; // The status code that should be used for the PSR-7 response. $status_code = 401; return \Snicco\Component\Psr7ErrorHandler\HttpException::fromPrevious($e, $status_code, $response_headers); } } $identifier = new SplHashIdentifier(); $information_provider = InformationProviderWithTransformation::fromDefaultData( $identifier, new CustomAuthenticationTo404Transformer() // Last argument is variadic );
If you provide no ExceptionTransformers
every exception will be converted
to a HttpException
with status code 500
. (unless it's already an instance
of HttpException
)
The ExceptionDisplayer
An ExceptionDisplayer
is responsible for
displaying ExceptionInformation
.
An ExceptionDisplayer
has one content-type that it supports.
The ProductionExceptionHandler
accepts one or
more ExceptionDisplayers
and will determine the best displayer for the current request.
This package comes with two default displayers that will be used as a fallback:
FallbackHtmlDisplayer
, for requests where theAccept
header istext/html
.FallbackJsonDisplayer
, for requests where theAccept
header isapplication/json
.
The best displayer for the combination of exception/request is determined by
using DisplayerFilters
.
Out of the box this package comes with the following filters:
Delegating
, delegates to other filters.CanDisplay
, filters based on the return value ofExceptionDisplayer::canDisplay()
Verbosity
, filters based on the return value ofExceptionDisplayer::isVerbose()
and the verbosity level during the current request.ContentType
, filters based on the return value ofExceptionDisplayer::supportedContentType()
and theAccept
header of the current request. ! Important: This filter only performs very basic content negotiation. Content-negotiation is out of scope for this package and should be performed in a middleware.
Full working example
This is a working example of how you would instantiate the ProductionErrorHandler
,
preferably in your dependency-injection container.
use Psr\Log\LogLevel; use Snicco\Component\Psr7ErrorHandler\DisplayerFilter\CanDisplay; use Snicco\Component\Psr7ErrorHandler\DisplayerFilter\ContentType; use Snicco\Component\Psr7ErrorHandler\DisplayerFilter\Delegating; use Snicco\Component\Psr7ErrorHandler\DisplayerFilter\Verbosity; use Snicco\Component\Psr7ErrorHandler\Identifier\SplHashIdentifier; use Snicco\Component\Psr7ErrorHandler\Information\InformationProviderWithTransformation; use Snicco\Component\Psr7ErrorHandler\Log\RequestAwareLogger; use Snicco\Component\Psr7ErrorHandler\ProductionErrorHandler; // Use any PSR-7 response factory $psr_7_response_factory = new Nyholm\Psr7\Factory\Psr17Factory(); $request_aware_logger = new RequestAwareLogger( new Monolog\Logger(), // Use any PSR-3 logger ); $information_provider = InformationProviderWithTransformation::fromDefaultData( new SplHashIdentifier() ); $prefer_verbose = (bool) getenv('APP_DEBUG'); $displayer_filter = new Delegating( new ContentType(), new Verbosity($prefer_verbose), new CanDisplay(), ); $error_handler = new ProductionErrorHandler( $psr_7_response_factory, $request_aware_logger, $information_provider, $displayer_filter, // Custom exception displayers go here (variadic) )
Then use the instantiated error handler in a middleware like so:
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\MiddlewareInterface; use Snicco\Component\Psr7ErrorHandler\HttpErrorHandler; class ErrorHandlerMiddleware implements MiddlewareInterface { private HttpErrorHandler $error_handler; public function __construct(HttpErrorHandler $error_handler) { $this->error_handler = $error_handler; } public function process(ServerRequestInterface $request,RequestHandlerInterface $handler) : ResponseInterface{ try { return $handler->handle($request); }catch (Throwable $e) { return $this->error_handler->handle($e, $request); } } }
Exception utilities
User-facing exceptions
This package comes with a UserFacing
interface, which your custom exceptions can implement.
If an exception that implements UserFacing
is thrown, the return values of
UserFacing::safeTitle()
and UserFacing::safeMessage()
will be used to create
the ExceptionInformation
, instead of the default HTTP error messages that
might not make sense to your users.
The original exception message will be logged while your users get to see something they can relate (a little more) to.
HTTP exceptions
This packages comes with a generic HTTPException
class that you can throw in your HTTP
related code (mostly middleware).
This allows you to dictate the HTTP response code and optionally additional response headers.
The difference between using the HTTPException
class and using
an ExceptionTransformer
is that the latter is intended for your domain
exceptions while HTTPExceptions
should be thrown only in HTTP related code (like middleware
and Controllers).
Contributing
This repository is a read-only split of the development repo of the Snicco project.
This is how you can contribute.
Reporting issues and sending pull requests
Please report issues in the Snicco monorepo.
Security
If you discover a security vulnerability, please follow our disclosure procedure.