Skip to content

Commit

Permalink
[HttpKernel] Wrap the exceptions into HttpException instances
Browse files Browse the repository at this point in the history
  • Loading branch information
angelov committed Dec 18, 2022
1 parent d071389 commit 081b0da
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 95 deletions.
15 changes: 15 additions & 0 deletions src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\HttpException as HttpExceptionAttribute;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
Expand Down Expand Up @@ -72,6 +73,20 @@ public function logKernelException(ExceptionEvent $event)
break;
}

// There's no specific status code defined in the configuration for this exception
if (!$throwable instanceof HttpExceptionInterface) {
$class = new \ReflectionClass($throwable);
$attributes = $class->getAttributes(HttpExceptionAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);

if (\count($attributes)) {
/** @var HttpExceptionAttribute $instance */
$instance = $attributes[0]->newInstance();

$throwable = new HttpException($instance->statusCode, $throwable->getMessage(), $throwable, $instance->headers);
$event->setThrowable($throwable);
}
}

$e = FlattenException::createFromThrowable($throwable);

$this->logException($throwable, sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()), $logLevel);
Expand Down
14 changes: 1 addition & 13 deletions src/Symfony/Component/HttpKernel/HttpKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\HttpException;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
Expand Down Expand Up @@ -240,18 +239,7 @@ private function handleThrowable(\Throwable $e, Request $request, int $type): Re
$response->setStatusCode($e->getStatusCode());
$response->headers->add($e->getHeaders());
} else {
$class = new \ReflectionClass($e);
$attributes = $class->getAttributes(HttpException::class, \ReflectionAttribute::IS_INSTANCEOF);

if (0 === \count($attributes)) {
$response->setStatusCode(500);
} else {
/** @var HttpException $instance */
$instance = $attributes[0]->newInstance();

$response->setStatusCode($instance->statusCode);
$response->headers->add($instance->headers);
}
$response->setStatusCode(500);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\HttpException;
use Symfony\Component\HttpKernel\Attribute\HttpException\NotFound;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
Expand Down Expand Up @@ -117,6 +119,36 @@ public function testHandleWithLoggerAndCustomConfiguration()
$this->assertCount(1, $logger->getLogs('warning'));
}

/**
* @dataProvider exceptionWithAttributeProvider
*/
public function testHandleHttpAttribute(\Throwable $exception, int $expectedStatusCode, array $expectedHeaders)
{
$request = new Request();
$event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $exception);
$l = new ErrorListener('not used');
$l->logKernelException($event);
$l->onKernelException($event);

$this->assertEquals(new Response('foo', $expectedStatusCode, $expectedHeaders), $event->getResponse());
}

public function testHandleCustomConfigurationAndHttpAttribute()
{
$request = new Request();
$event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new NotFoundHttpExceptionWithAttribute());
$l = new ErrorListener('not used', null, false, [
NotFoundHttpExceptionWithAttribute::class => [
'log_level' => 'warning',
'status_code' => 401,
],
]);
$l->logKernelException($event);
$l->onKernelException($event);

$this->assertEquals(new Response('foo', 401), $event->getResponse());
}

public function provider()
{
if (!class_exists(Request::class)) {
Expand Down Expand Up @@ -216,6 +248,13 @@ public function controllerProvider()
return new Response('OK: '.$exception->getMessage());
}];
}

public function exceptionWithAttributeProvider()
{
yield [new NotFoundHttpExceptionWithAttribute(), 404, ['resource' => 'example']];
yield [new WithCustomUserProvidedAttribute(), 208, ['name' => 'value']];
yield [new WithGeneralAttribute(), 412, ['some' => 'thing']];
}
}

class TestLogger extends Logger implements DebugLoggerInterface
Expand Down Expand Up @@ -246,3 +285,41 @@ public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = tr
throw new \RuntimeException('bar');
}
}

#[NotFound(
headers: [
'resource' => 'example',
]
)]
class NotFoundHttpExceptionWithAttribute extends \Exception
{
}

#[\Attribute(\Attribute::TARGET_CLASS)]
class UserProvidedHttpStatusCodeAttribute extends HttpException
{
public function __construct(array $headers = [])
{
parent::__construct(
Response::HTTP_ALREADY_REPORTED,
$headers
);
}
}

#[UserProvidedHttpStatusCodeAttribute(headers: [
'name' => 'value',
])]
class WithCustomUserProvidedAttribute extends \Exception
{
}

#[HttpException(
statusCode: Response::HTTP_PRECONDITION_FAILED,
headers: [
'some' => 'thing',
]
)]
class WithGeneralAttribute extends \Exception
{
}
82 changes: 0 additions & 82 deletions src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\HttpException;
use Symfony\Component\HttpKernel\Attribute\HttpException\NotFound;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
Expand Down Expand Up @@ -236,48 +234,6 @@ public function testHandleHttpException()
$this->assertEquals('POST', $response->headers->get('Allow'));
}

public function testHandleExceptionWithSpecificHttpAttribute()
{
$dispatcher = new EventDispatcher();
$dispatcher->addListener(KernelEvents::EXCEPTION, function (ExceptionEvent $event) {
$event->setResponse(new Response());
});

$kernel = $this->getHttpKernel($dispatcher, function () { throw new NotFoundHttpExceptionWithAttribute(); });
$response = $kernel->handle(new Request());

$this->assertEquals('404', $response->getStatusCode());
$this->assertEquals('example', $response->headers->get('resource'));
}

public function testHandleExceptionWithUserProvidedHttpAttribute()
{
$dispatcher = new EventDispatcher();
$dispatcher->addListener(KernelEvents::EXCEPTION, function (ExceptionEvent $event) {
$event->setResponse(new Response());
});

$kernel = $this->getHttpKernel($dispatcher, function () { throw new WithCustomUserProvidedAttribute(); });
$response = $kernel->handle(new Request());

$this->assertEquals('208', $response->getStatusCode());
$this->assertEquals('value', $response->headers->get('name'));
}

public function testHandleExceptionWithGeneralHttpAttribute()
{
$dispatcher = new EventDispatcher();
$dispatcher->addListener(KernelEvents::EXCEPTION, function (ExceptionEvent $event) {
$event->setResponse(new Response());
});

$kernel = $this->getHttpKernel($dispatcher, function () { throw new WithGeneralAttribute(); });
$response = $kernel->handle(new Request());

$this->assertEquals('412', $response->getStatusCode());
$this->assertEquals('thing', $response->headers->get('some'));
}

public function getStatusCodes()
{
return [
Expand Down Expand Up @@ -569,41 +525,3 @@ function controller_func()
{
return new Response('foo');
}

#[NotFound(
headers: [
'resource' => 'example',
]
)]
class NotFoundHttpExceptionWithAttribute extends \Exception
{
}

#[\Attribute(\Attribute::TARGET_CLASS)]
class UserProvidedHttpStatusCodeAttribute extends HttpException
{
public function __construct(array $headers = [])
{
parent::__construct(
Response::HTTP_ALREADY_REPORTED,
$headers
);
}
}

#[UserProvidedHttpStatusCodeAttribute(headers: [
'name' => 'value',
])]
class WithCustomUserProvidedAttribute extends \Exception
{
}

#[HttpException(
statusCode: Response::HTTP_PRECONDITION_FAILED,
headers: [
'some' => 'thing',
]
)]
class WithGeneralAttribute extends \Exception
{
}

0 comments on commit 081b0da

Please sign in to comment.