Skip to content

Commit

Permalink
[HttpKernel] Allow using #[HttpStatus] for setting status code and …
Browse files Browse the repository at this point in the history
…headers for HTTP exceptions
  • Loading branch information
angelov authored and nicolas-grekas committed Dec 21, 2022
1 parent a27e37a commit d1cbcca
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 0 deletions.
28 changes: 28 additions & 0 deletions src/Symfony/Component/HttpKernel/Attribute/HttpStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpKernel\Attribute;

/**
* @author Dejan Angelov <angelovdejan@protonmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class HttpStatus
{
/**
* @param array<string, string> $headers
*/
public function __construct(
public readonly int $statusCode,
public readonly array $headers = [],
) {
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Deprecate parameters `container.dumper.inline_factories` and `container.dumper.inline_class_loader`, use `.container.dumper.inline_factories` and `.container.dumper.inline_class_loader` instead
* `FileProfilerStorage` removes profiles automatically after two days
* Add `#[HttpStatus]` for defining status codes for exceptions

6.2
---
Expand Down
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\HttpStatus;
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(HttpStatus::class, \ReflectionAttribute::IS_INSTANCEOF);

if ($attributes) {
/** @var HttpStatus $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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\HttpStatus;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
Expand Down Expand Up @@ -117,6 +118,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 WithGeneralAttribute());
$l = new ErrorListener('not used', null, false, [
WithGeneralAttribute::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 +247,12 @@ public function controllerProvider()
return new Response('OK: '.$exception->getMessage());
}];
}

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

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

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

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

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

0 comments on commit d1cbcca

Please sign in to comment.