Move error handling to dedicated controller instead of middleware

This commit is contained in:
ArthurHoaro 2020-08-21 10:50:44 +02:00
parent bedbb845ee
commit 0c6fdbe12b
5 changed files with 135 additions and 40 deletions

View file

@ -9,6 +9,7 @@
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Feed\FeedBuilder; use Shaarli\Feed\FeedBuilder;
use Shaarli\Formatter\FormatterFactory; use Shaarli\Formatter\FormatterFactory;
use Shaarli\Front\Controller\Visitor\ErrorController;
use Shaarli\History; use Shaarli\History;
use Shaarli\Http\HttpAccess; use Shaarli\Http\HttpAccess;
use Shaarli\Netscape\NetscapeBookmarkUtils; use Shaarli\Netscape\NetscapeBookmarkUtils;
@ -148,6 +149,10 @@ public function build(): ShaarliContainer
); );
}; };
$container['errorHandler'] = function (ShaarliContainer $container): ErrorController {
return new ErrorController($container);
};
return $container; return $container;
} }
} }

View file

@ -3,7 +3,6 @@
namespace Shaarli\Front; namespace Shaarli\Front;
use Shaarli\Container\ShaarliContainer; use Shaarli\Container\ShaarliContainer;
use Shaarli\Front\Exception\ShaarliFrontException;
use Shaarli\Front\Exception\UnauthorizedException; use Shaarli\Front\Exception\UnauthorizedException;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
@ -53,35 +52,12 @@ public function __invoke(Request $request, Response $response, callable $next):
$this->checkOpenShaarli($request, $response, $next); $this->checkOpenShaarli($request, $response, $next);
return $next($request, $response); return $next($request, $response);
} catch (ShaarliFrontException $e) {
// Possible functional error
$this->container->pageBuilder->reset();
$this->container->pageBuilder->assign('message', nl2br($e->getMessage()));
$response = $response->withStatus($e->getCode());
return $response->write($this->container->pageBuilder->render('error', $this->container->basePath));
} catch (UnauthorizedException $e) { } catch (UnauthorizedException $e) {
$returnUrl = urlencode($this->container->environment['REQUEST_URI']); $returnUrl = urlencode($this->container->environment['REQUEST_URI']);
return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl); return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl);
} catch (\Throwable $e) {
// Unknown error encountered
$this->container->pageBuilder->reset();
if ($this->container->conf->get('dev.debug', false)) {
$this->container->pageBuilder->assign('message', $e->getMessage());
$this->container->pageBuilder->assign(
'stacktrace',
nl2br(get_class($e) .': '. PHP_EOL . $e->getTraceAsString())
);
} else {
$this->container->pageBuilder->assign('message', t('An unexpected error occurred.'));
}
$response = $response->withStatus(500);
return $response->write($this->container->pageBuilder->render('error', $this->container->basePath));
} }
// Other exceptions are handled by ErrorController
} }
/** /**

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller\Visitor;
use Shaarli\Front\Exception\ShaarliFrontException;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Controller used to render the error page, with a provided exception.
* It is actually used as a Slim error handler.
*/
class ErrorController extends ShaarliVisitorController
{
public function __invoke(Request $request, Response $response, \Throwable $throwable): Response
{
// Unknown error encountered
$this->container->pageBuilder->reset();
if ($throwable instanceof ShaarliFrontException) {
// Functional error
$this->assignView('message', nl2br($throwable->getMessage()));
$response = $response->withStatus($throwable->getCode());
} else {
// Internal error (any other Throwable)
if ($this->container->conf->get('dev.debug', false)) {
$this->assignView('message', $throwable->getMessage());
$this->assignView(
'stacktrace',
nl2br(get_class($throwable) .': '. PHP_EOL . $throwable->getTraceAsString())
);
} else {
$this->assignView('message', t('An unexpected error occurred.'));
}
$response = $response->withStatus(500);
}
return $response->write($this->render('error'));
}
}

View file

@ -74,7 +74,8 @@ public function testMiddlewareExecution(): void
} }
/** /**
* Test middleware execution with controller throwing a known front exception * Test middleware execution with controller throwing a known front exception.
* The exception should be thrown to be later handled by the error handler.
*/ */
public function testMiddlewareExecutionWithFrontException(): void public function testMiddlewareExecutionWithFrontException(): void
{ {
@ -99,16 +100,14 @@ public function testMiddlewareExecutionWithFrontException(): void
}); });
$this->container->pageBuilder = $pageBuilder; $this->container->pageBuilder = $pageBuilder;
/** @var Response $result */ $this->expectException(LoginBannedException::class);
$result = $this->middleware->__invoke($request, $response, $controller);
static::assertInstanceOf(Response::class, $result); $this->middleware->__invoke($request, $response, $controller);
static::assertSame(401, $result->getStatusCode());
static::assertContains('error', (string) $result->getBody());
} }
/** /**
* Test middleware execution with controller throwing a not authorized exception * Test middleware execution with controller throwing a not authorized exception
* The middle should send a redirection response to the login page.
*/ */
public function testMiddlewareExecutionWithUnauthorizedException(): void public function testMiddlewareExecutionWithUnauthorizedException(): void
{ {
@ -136,9 +135,10 @@ public function testMiddlewareExecutionWithUnauthorizedException(): void
} }
/** /**
* Test middleware execution with controller throwing a not authorized exception * Test middleware execution with controller throwing a not authorized exception.
* The exception should be thrown to be later handled by the error handler.
*/ */
public function testMiddlewareExecutionWithServerExceptionWith(): void public function testMiddlewareExecutionWithServerException(): void
{ {
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
$request->method('getUri')->willReturnCallback(function (): Uri { $request->method('getUri')->willReturnCallback(function (): Uri {
@ -148,9 +148,11 @@ public function testMiddlewareExecutionWithServerExceptionWith(): void
return $uri; return $uri;
}); });
$dummyException = new class() extends \Exception {};
$response = new Response(); $response = new Response();
$controller = function (): void { $controller = function () use ($dummyException): void {
throw new \Exception(); throw $dummyException;
}; };
$parameters = []; $parameters = [];
@ -165,12 +167,9 @@ public function testMiddlewareExecutionWithServerExceptionWith(): void
}) })
; ;
/** @var Response $result */ $this->expectException(get_class($dummyException));
$result = $this->middleware->__invoke($request, $response, $controller);
static::assertSame(500, $result->getStatusCode()); $this->middleware->__invoke($request, $response, $controller);
static::assertContains('error', (string) $result->getBody());
static::assertSame('An unexpected error occurred.', $parameters['message']);
} }
public function testMiddlewareExecutionWithUpdates(): void public function testMiddlewareExecutionWithUpdates(): void

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller\Visitor;
use PHPUnit\Framework\TestCase;
use Shaarli\Front\Exception\ShaarliFrontException;
use Slim\Http\Request;
use Slim\Http\Response;
class ErrorControllerTest extends TestCase
{
use FrontControllerMockHelper;
/** @var ErrorController */
protected $controller;
public function setUp(): void
{
$this->createContainer();
$this->controller = new ErrorController($this->container);
}
/**
* Test displaying error with a ShaarliFrontException: display exception message and use its code for HTTTP code
*/
public function testDisplayFrontExceptionError(): void
{
$request = $this->createMock(Request::class);
$response = new Response();
$message = 'error message';
$errorCode = 418;
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$result = ($this->controller)(
$request,
$response,
new class($message, $errorCode) extends ShaarliFrontException {}
);
static::assertSame($errorCode, $result->getStatusCode());
static::assertSame($message, $assignedVariables['message']);
static::assertArrayNotHasKey('stacktrace', $assignedVariables);
}
/**
* Test displaying error with any exception (no debug): only display an error occurred with HTTP 500.
*/
public function testDisplayAnyExceptionErrorNoDebug(): void
{
$request = $this->createMock(Request::class);
$response = new Response();
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$result = ($this->controller)($request, $response, new \Exception('abc'));
static::assertSame(500, $result->getStatusCode());
static::assertSame('An unexpected error occurred.', $assignedVariables['message']);
static::assertArrayNotHasKey('stacktrace', $assignedVariables);
}
}