Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions src/Laravel/ApiPlatformDeferredProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
use ApiPlatform\Laravel\Eloquent\State\PersistProcessor;
use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor;
use ApiPlatform\Laravel\Exception\ErrorHandler;
use ApiPlatform\Laravel\Exception\ErrorRenderer;
use ApiPlatform\Laravel\Metadata\CacheResourceCollectionMetadataFactory;
use ApiPlatform\Laravel\Metadata\ParameterValidationResourceMetadataCollectionFactory;
use ApiPlatform\Laravel\State\ParameterValidatorProvider;
Expand Down Expand Up @@ -272,22 +273,35 @@ public function register(): void
);
});

$this->app->singleton(ErrorRenderer::class, static function (Application $app) {
/** @var ConfigRepository */
$config = $app['config'];

return new ErrorRenderer(
$app->make(ResourceMetadataCollectionFactoryInterface::class),
$app->make(ApiPlatformController::class),
$app->make(IdentifiersExtractorInterface::class),
$app->make(ResourceClassResolverInterface::class),
$app->make(Negotiator::class),
$config->get('api-platform.exception_to_status'),
$config->get('app.debug'),
$config->get('api-platform.error_formats'),
);
});

$this->app->extend(
ExceptionHandler::class,
static function (ExceptionHandler $decorated, Application $app) {
/** @var ConfigRepository */
$config = $app['config'];

if (!$config->get('api-platform.error_handler.extend_laravel_handler', true)) {
return $decorated;
}

return new ErrorHandler(
$app,
$app->make(ResourceMetadataCollectionFactoryInterface::class),
$app->make(ApiPlatformController::class),
$app->make(IdentifiersExtractorInterface::class),
$app->make(ResourceClassResolverInterface::class),
$app->make(Negotiator::class),
$config->get('api-platform.exception_to_status'),
$config->get('app.debug'),
$config->get('api-platform.error_formats'),
$app->make(ErrorRenderer::class),
$decorated
);
}
Expand Down
210 changes: 11 additions & 199 deletions src/Laravel/Exception/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,229 +13,41 @@

namespace ApiPlatform\Laravel\Exception;

use ApiPlatform\Laravel\ApiResource\Error;
use ApiPlatform\Laravel\Controller\ApiPlatformController;
use ApiPlatform\Metadata\Exception\InvalidUriVariableException;
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
use ApiPlatform\Metadata\Exception\StatusAwareExceptionInterface;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\Util\ContentNegotiationTrait;
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Foundation\Exceptions\Handler as ExceptionsHandler;
use Negotiation\Negotiator;
use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;

class ErrorHandler extends ExceptionsHandler
{
use ContentNegotiationTrait;
use OperationRequestInitiatorTrait;

public static mixed $error;

/**
* @param array<class-string, int> $exceptionToStatus
* @param array<string, string[]> $errorFormats
*/
public function __construct(
Container $container,
ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
private readonly ApiPlatformController $apiPlatformController,
private readonly ?IdentifiersExtractorInterface $identifiersExtractor = null,
private readonly ?ResourceClassResolverInterface $resourceClassResolver = null,
?Negotiator $negotiator = null,
private readonly ?array $exceptionToStatus = null,
private readonly ?bool $debug = false,
private readonly ?array $errorFormats = null,
private readonly ErrorRenderer $errorRenderer,
private readonly ?ExceptionHandler $decorated = null,
) {
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
$this->negotiator = $negotiator;
parent::__construct($container);
}

public function render($request, \Throwable $exception)
{
$apiOperation = $this->initializeOperation($request);

if (!$apiOperation) {
// For non-API operations, first check if any renderable callbacks on this
// ErrorHandler instance can handle the exception (issue #7466).
$response = $this->renderViaCallbacks($request, $exception);
if ($this->errorRenderer->shouldRender($request, $exception)) {
$response = $this->errorRenderer->render($request, $exception);

if ($response) {
return $response;
}

// If no callbacks handled it, delegate to the decorated handler if available
// to preserve custom exception handler classes (issue #7058).
return $this->decorated ? $this->decorated->render($request, $exception) : parent::render($request, $exception);
}

$formats = $this->errorFormats ?? ['jsonproblem' => ['application/problem+json']];
$format = $request->getRequestFormat() ?? $this->getRequestFormat($request, $formats, false);

if ($this->resourceClassResolver->isResourceClass($exception::class)) {
$resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class);

$operation = null;
foreach ($resourceCollection as $resource) {
foreach ($resource->getOperations() as $op) {
foreach ($op->getOutputFormats() ?? [] as $key => $value) {
if ($key === $format) {
$operation = $op;
break 3;
}
}
}
}

// No operation found for the requested format, we take the first available
if (!$operation) {
$operation = $resourceCollection->getOperation();
}
$errorResource = $exception;
if ($errorResource instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) {
$statusCode = $this->getStatusCode($apiOperation, $operation, $exception);
$operation = $operation->withStatus($statusCode);
if ($errorResource instanceof StatusAwareExceptionInterface) {
$errorResource->setStatus($statusCode);
}
}
} else {
// Create a generic, rfc7807 compatible error according to the wanted format
$operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format));
// status code may be overridden by the exceptionToStatus option
$statusCode = 500;
if ($operation instanceof HttpOperation) {
$statusCode = $this->getStatusCode($apiOperation, $operation, $exception);
$operation = $operation->withStatus($statusCode);
}

$errorResource = Error::createFromException($exception, $statusCode);
}

/** @var HttpOperation $operation */
if (!$operation->getProvider()) {
static::$error = $errorResource;
$operation = $operation->withProvider([self::class, 'provide']);
}

// For our swagger Ui errors
if ('html' === $format) {
$operation = $operation->withOutputFormats(['html' => ['text/html']]);
}

$identifiers = [];
try {
$identifiers = $this->identifiersExtractor?->getIdentifiersFromItem($errorResource, $operation) ?? [];
} catch (\Exception $e) {
}

$normalizationContext = $operation->getNormalizationContext() ?? [];
if (!($normalizationContext['api_error_resource'] ?? false)) {
$normalizationContext += ['api_error_resource' => true];
}

if (!isset($normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES])) {
$normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES] = true === $this->debug ? [] : ['originalTrace'];
}

$operation = $operation->withNormalizationContext($normalizationContext);

$dup = $request->duplicate(null, null, []);
$dup->setMethod('GET');
$dup->attributes->set('_api_resource_class', $operation->getClass());
$dup->attributes->set('_api_previous_operation', $apiOperation);
$dup->attributes->set('_api_operation', $operation);
$dup->attributes->set('_api_operation_name', $operation->getName());
$dup->attributes->set('exception', $exception);
// These are for swagger
$dup->attributes->set('_api_original_route', $request->attributes->get('_route'));
$dup->attributes->set('_api_original_uri_variables', $request->attributes->get('_api_uri_variables'));
$dup->attributes->set('_api_original_route_params', $request->attributes->get('_route_params'));
$dup->attributes->set('_api_requested_operation', $request->attributes->get('_api_requested_operation'));

foreach ($identifiers as $name => $value) {
$dup->attributes->set($name, $value);
}

try {
$response = $this->apiPlatformController->__invoke($dup);
// If it's not an API operation, or the renderer wasn't able to generate
// a response, first check if any renderable callbacks on this ErrorHandler
// instance can handle the exception (issue #7466).
$response = $this->renderViaCallbacks($request, $exception);

if ($response) {
return $response;
} catch (\Throwable $e) {
return $this->decorated ? $this->decorated->render($request, $exception) : parent::render($request, $exception);
}
}

private function getStatusCode(?HttpOperation $apiOperation, ?HttpOperation $errorOperation, \Throwable $exception): int
{
$exceptionToStatus = array_merge(
$apiOperation ? $apiOperation->getExceptionToStatus() ?? [] : [],
$errorOperation ? $errorOperation->getExceptionToStatus() ?? [] : [],
$this->exceptionToStatus ?? []
);

foreach ($exceptionToStatus as $class => $status) {
if (is_a($exception::class, $class, true)) {
return $status;
}
}

if ($exception instanceof AuthenticationException) {
return 401;
}

if ($exception instanceof AuthorizationException) {
return 403;
}

if ($exception instanceof SymfonyHttpExceptionInterface) {
return $exception->getStatusCode();
}

if ($exception instanceof RequestExceptionInterface || $exception instanceof InvalidUriVariableException) {
return 400;
}

// if ($exception instanceof ValidationException) {
// return 422;
// }

if ($status = $errorOperation?->getStatus()) {
return $status;
}

return 500;
}

private function getFormatOperation(?string $format): string
{
return match ($format) {
'json' => '_api_errors_problem',
'jsonproblem' => '_api_errors_problem',
'jsonld' => '_api_errors_hydra',
'jsonapi' => '_api_errors_jsonapi',
'html' => '_api_errors_problem', // This will be intercepted by the SwaggerUiProvider
default => '_api_errors_problem',
};
}

public static function provide(): mixed
{
if ($data = static::$error) {
return $data;
}

throw new \LogicException(\sprintf('We could not find the thrown exception in the %s.', self::class));
// If no callbacks handled it, delegate to the decorated handler if available
// to preserve custom exception handler classes (issue #7058).
return $this->decorated ? $this->decorated->render($request, $exception) : parent::render($request, $exception);
}
}
Loading
Loading