Created middleware to keep backwards compatibility on errors when using v1 and 2 of the API

This commit is contained in:
Alejandro Celaya
2022-08-13 16:50:19 +02:00
parent ed7be6eb99
commit cd4fe4362b
6 changed files with 183 additions and 1 deletions

View File

@@ -26,6 +26,7 @@ return [
'path' => '/rest',
'middleware' => [
ProblemDetails\ProblemDetailsMiddleware::class,
Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class,
],
],

View File

@@ -21,7 +21,7 @@ class ValidationException extends InvalidArgumentException implements ProblemDet
use CommonProblemDetailsExceptionTrait;
private const TITLE = 'Invalid data';
private const TYPE = 'INVALID_ARGUMENT';
public const TYPE = 'https://shlink.io/api/error/invalid-data';
private array $invalidElements;

View File

@@ -53,6 +53,7 @@ return [
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class => ConfigAbstractFactory::class,
Middleware\ShortUrl\OverrideDomainMiddleware::class => ConfigAbstractFactory::class,
Middleware\Mercure\NotConfiguredMercureErrorHandler::class => ConfigAbstractFactory::class,
Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class => InvokableFactory::class,
],
],

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Exception;
use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
use Shlinkio\Shlink\Core\Exception\ValidationException;
/** @deprecated */
class BackwardsCompatibleProblemDetailsException extends RuntimeException implements ProblemDetailsExceptionInterface
{
private function __construct(private readonly ProblemDetailsExceptionInterface $e)
{
parent::__construct($e->getMessage(), $e->getCode(), $e);
}
public static function fromProblemDetails(ProblemDetailsExceptionInterface $e): self
{
return new self($e);
}
public function getStatus(): int
{
return $this->e->getStatus();
}
public function getType(): string
{
return $this->remapType($this->e->getType());
}
public function getTitle(): string
{
return $this->e->getTitle();
}
public function getDetail(): string
{
return $this->e->getDetail();
}
public function getAdditionalData(): array
{
return $this->e->getAdditionalData();
}
public function toArray(): array
{
return $this->remapTypeInArray($this->e->toArray());
}
public function jsonSerialize(): array
{
return $this->remapTypeInArray($this->e->jsonSerialize());
}
private function remapTypeInArray(array $wrappedArray): array
{
if (! isset($wrappedArray['type'])) {
return $wrappedArray;
}
return [...$wrappedArray, 'type' => $this->remapType($wrappedArray['type'])];
}
private function remapType(string $wrappedType): string
{
return match ($wrappedType) {
ValidationException::TYPE => 'INVALID_ARGUMENT',
default => $wrappedType,
};
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware\ErrorHandler;
use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Exception\BackwardsCompatibleProblemDetailsException;
use function version_compare;
/** @deprecated */
class BackwardsCompatibleProblemDetailsHandler implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);
} catch (ProblemDetailsExceptionInterface $e) {
$version = $request->getAttribute('version') ?? '2';
throw version_compare($version, '3', '>=')
? $e
: BackwardsCompatibleProblemDetailsException::fromProblemDetails($e);
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware\ErrorHandler;
use Laminas\Diactoros\ServerRequestFactory;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Rest\Exception\BackwardsCompatibleProblemDetailsException;
use Shlinkio\Shlink\Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler;
use Throwable;
class BackwardsCompatibleProblemDetailsHandlerTest extends TestCase
{
use ProphecyTrait;
private BackwardsCompatibleProblemDetailsHandler $handler;
protected function setUp(): void
{
$this->handler = new BackwardsCompatibleProblemDetailsHandler();
}
/**
* @test
* @dataProvider provideExceptions
*/
public function expectedExceptionIsThrownBasedOnTheRequestVersion(
ServerRequestInterface $request,
Throwable $thrownException,
string $expectedException,
): void {
$handler = $this->prophesize(RequestHandlerInterface::class);
$handle = $handler->handle($request)->willThrow($thrownException);
$this->expectException($expectedException);
$handle->shouldBeCalledOnce();
$this->handler->process($request, $handler->reveal());
}
public function provideExceptions(): iterable
{
$baseRequest = ServerRequestFactory::fromGlobals();
yield 'no version' => [
$baseRequest,
ValidationException::fromArray([]),
BackwardsCompatibleProblemDetailsException::class,
];
yield 'version 1' => [
$baseRequest->withAttribute('version', '1'),
ValidationException::fromArray([]),
BackwardsCompatibleProblemDetailsException::class,
];
yield 'version 2' => [
$baseRequest->withAttribute('version', '2'),
ValidationException::fromArray([]),
BackwardsCompatibleProblemDetailsException::class,
];
yield 'version 3' => [
$baseRequest->withAttribute('version', '3'),
ValidationException::fromArray([]),
ValidationException::class,
];
yield 'version 4' => [
$baseRequest->withAttribute('version', '3'),
ValidationException::fromArray([]),
ValidationException::class,
];
}
}