diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index c628c4fd..5291db8c 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -26,6 +26,7 @@ return [ 'path' => '/rest', 'middleware' => [ ProblemDetails\ProblemDetailsMiddleware::class, + Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class, ], ], diff --git a/module/Core/src/Exception/ValidationException.php b/module/Core/src/Exception/ValidationException.php index 326eec11..0f625bc5 100644 --- a/module/Core/src/Exception/ValidationException.php +++ b/module/Core/src/Exception/ValidationException.php @@ -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; diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 189180b0..a70cb7f1 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -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, ], ], diff --git a/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php new file mode 100644 index 00000000..ddc3768f --- /dev/null +++ b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php @@ -0,0 +1,74 @@ +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, + }; + } +} diff --git a/module/Rest/src/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandler.php b/module/Rest/src/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandler.php new file mode 100644 index 00000000..c099ad70 --- /dev/null +++ b/module/Rest/src/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandler.php @@ -0,0 +1,30 @@ +handle($request); + } catch (ProblemDetailsExceptionInterface $e) { + $version = $request->getAttribute('version') ?? '2'; + throw version_compare($version, '3', '>=') + ? $e + : BackwardsCompatibleProblemDetailsException::fromProblemDetails($e); + } + } +} diff --git a/module/Rest/test/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandlerTest.php b/module/Rest/test/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandlerTest.php new file mode 100644 index 00000000..00dddb2f --- /dev/null +++ b/module/Rest/test/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandlerTest.php @@ -0,0 +1,76 @@ +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, + ]; + } +}