-
-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ExceptionHandler interface and middleware (#375)
- Loading branch information
Showing
7 changed files
with
239 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Amp\Http\Server; | ||
|
||
use Amp\Http\HttpStatus; | ||
use Psr\Log\LoggerInterface as PsrLogger; | ||
|
||
/** | ||
* Simple exception handler that writes a message to the logger and returns an error page generated by the provided | ||
* {@see ErrorHandler}. | ||
*/ | ||
final class DefaultExceptionHandler implements ExceptionHandler | ||
{ | ||
public function __construct( | ||
private readonly ErrorHandler $errorHandler, | ||
private readonly PsrLogger $logger, | ||
) { | ||
} | ||
|
||
public function handleException(Request $request, \Throwable $exception): Response | ||
{ | ||
$client = $request->getClient(); | ||
$method = $request->getMethod(); | ||
$uri = (string) $request->getUri(); | ||
$protocolVersion = $request->getProtocolVersion(); | ||
$local = $client->getLocalAddress()->toString(); | ||
$remote = $client->getRemoteAddress()->toString(); | ||
|
||
$this->logger->error( | ||
\sprintf( | ||
"Unexpected %s with message '%s' thrown from %s:%d when handling request: %s %s HTTP/%s %s on %s", | ||
$exception::class, | ||
$exception->getMessage(), | ||
$exception->getFile(), | ||
$exception->getLine(), | ||
$method, | ||
$uri, | ||
$protocolVersion, | ||
$remote, | ||
$local, | ||
), | ||
[ | ||
'exception' => $exception, | ||
'method' => $method, | ||
'uri' => $uri, | ||
'protocolVersion' => $protocolVersion, | ||
'local' => $local, | ||
'remote' => $remote, | ||
], | ||
); | ||
|
||
return $this->errorHandler->handleError(HttpStatus::INTERNAL_SERVER_ERROR, request: $request); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Amp\Http\Server\Driver\Internal; | ||
|
||
use Amp\Http\Server\DefaultErrorHandler; | ||
use Amp\Http\Server\ErrorHandler; | ||
use Amp\Http\Server\Request; | ||
use Amp\Http\Server\Response; | ||
use Psr\Log\LoggerInterface as PsrLogger; | ||
|
||
/** @internal */ | ||
final class HttpDriverErrorHandler implements ErrorHandler | ||
{ | ||
private static ?DefaultErrorHandler $defaultErrorHandler = null; | ||
|
||
private static function getDefaultErrorHandler(): ErrorHandler | ||
{ | ||
return self::$defaultErrorHandler ??= new DefaultErrorHandler(); | ||
} | ||
|
||
public function __construct( | ||
private readonly ErrorHandler $errorHandler, | ||
private readonly PsrLogger $logger, | ||
) { | ||
} | ||
|
||
public function handleError(int $status, ?string $reason = null, ?Request $request = null): Response | ||
{ | ||
try { | ||
return $this->errorHandler->handleError($status, $reason, $request); | ||
} catch (\Throwable $exception) { | ||
// If the error handler throws, fallback to returning the default error page. | ||
$this->logger->error( | ||
\sprintf( | ||
"Unexpected %s thrown from %s::handleError(), falling back to default error handler.", | ||
$exception::class, | ||
$this->errorHandler::class, | ||
), | ||
['exception' => $exception], | ||
); | ||
|
||
// The default error handler will never throw, otherwise there's a bug | ||
return self::getDefaultErrorHandler()->handleError($status, null, $request); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Amp\Http\Server; | ||
|
||
use Amp\Http\Server\Middleware\ExceptionHandlerMiddleware; | ||
|
||
interface ExceptionHandler | ||
{ | ||
/** | ||
* Handles an uncaught exception from the {@see RequestHandler} wrapped with {@see ExceptionHandlerMiddleware}. | ||
*/ | ||
public function handleException(Request $request, \Throwable $exception): Response; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Amp\Http\Server\Middleware; | ||
|
||
use Amp\Http\Server\ClientException; | ||
use Amp\Http\Server\ExceptionHandler; | ||
use Amp\Http\Server\HttpErrorException; | ||
use Amp\Http\Server\Middleware; | ||
use Amp\Http\Server\Request; | ||
use Amp\Http\Server\RequestHandler; | ||
use Amp\Http\Server\Response; | ||
|
||
/** | ||
* This middleware catches exceptions from the wrapped {@see RequestHandler}, delegating handling of the exception to | ||
* the provided instance of {@see ExceptionHandler}. Generally it is recommended that this middleware be first in the | ||
* middleware stack so it is able to catch any exception from another middleware or request handler. | ||
*/ | ||
final class ExceptionHandlerMiddleware implements Middleware | ||
{ | ||
public function __construct(private readonly ExceptionHandler $exceptionHandler) | ||
{ | ||
} | ||
|
||
public function handleRequest(Request $request, RequestHandler $requestHandler): Response | ||
{ | ||
try { | ||
return $requestHandler->handleRequest($request); | ||
} catch (ClientException|HttpErrorException $exception) { | ||
// Rethrow our special client exception or HTTP error exception. These exceptions have special meaning | ||
// to the HTTP driver, so will be handled differently from other uncaught exceptions from the request | ||
// handler. | ||
throw $exception; | ||
} catch (\Throwable $exception) { | ||
return $this->exceptionHandler->handleException($request, $exception); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Amp\Http\Server\Test\Middleware; | ||
|
||
use Amp\Http\HttpStatus; | ||
use Amp\Http\Server\Driver\Client; | ||
use Amp\Http\Server\ExceptionHandler; | ||
use Amp\Http\Server\HttpErrorException; | ||
use Amp\Http\Server\Middleware\ExceptionHandlerMiddleware; | ||
use Amp\Http\Server\Request; | ||
use Amp\Http\Server\RequestHandler; | ||
use Amp\Http\Server\Response; | ||
use Amp\PHPUnit\AsyncTestCase; | ||
use Amp\PHPUnit\TestException; | ||
use League\Uri\Http; | ||
|
||
class ExceptionHandlerMiddlewareTest extends AsyncTestCase | ||
{ | ||
private function setupAndInvokeMiddleware(ExceptionHandler $exceptionHandler, \Throwable $exception): Response | ||
{ | ||
$request = new Request($this->createMock(Client::class), 'GET', Http::createFromString('/')); | ||
|
||
$requestHandler = $this->createMock(RequestHandler::class); | ||
$requestHandler->expects(self::once()) | ||
->method('handleRequest') | ||
->with($request) | ||
->willThrowException($exception); | ||
|
||
$middleware = new ExceptionHandlerMiddleware($exceptionHandler); | ||
|
||
return $middleware->handleRequest($request, $requestHandler); | ||
} | ||
|
||
public function testUncaughtException(): void | ||
{ | ||
$exception = new TestException(); | ||
|
||
$exceptionHandler = $this->createMock(ExceptionHandler::class); | ||
$exceptionHandler->expects(self::once()) | ||
->method('handleException') | ||
->with(self::isInstanceOf(Request::class), $exception) | ||
->willReturn(new Response(HttpStatus::INTERNAL_SERVER_ERROR)); | ||
|
||
$this->setupAndInvokeMiddleware($exceptionHandler, $exception); | ||
} | ||
|
||
public function testHttpErrorException(): void | ||
{ | ||
$exception = new HttpErrorException(HttpStatus::BAD_REQUEST); | ||
|
||
$exceptionHandler = $this->createMock(ExceptionHandler::class); | ||
$exceptionHandler->expects(self::never()) | ||
->method('handleException'); | ||
|
||
$this->expectExceptionObject($exception); | ||
|
||
$this->setupAndInvokeMiddleware($exceptionHandler, $exception); | ||
} | ||
} |