Skip to content

Commit b0fa229

Browse files
authoredMar 28, 2025··
fix(laravel): error handler only on api routes (#7049)
1 parent eaf0075 commit b0fa229

File tree

4 files changed

+113
-111
lines changed

4 files changed

+113
-111
lines changed
 

Diff for: ‎src/Laravel/ApiPlatformDeferredProvider.php

-24
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
use ApiPlatform\GraphQl\Type\TypesContainerInterface;
2323
use ApiPlatform\JsonApi\Filter\SparseFieldset;
2424
use ApiPlatform\JsonApi\Filter\SparseFieldsetParameterProvider;
25-
use ApiPlatform\Laravel\Controller\ApiPlatformController;
2625
use ApiPlatform\Laravel\Eloquent\Extension\FilterQueryExtension;
2726
use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface;
2827
use ApiPlatform\Laravel\Eloquent\Filter\BooleanFilter;
@@ -41,12 +40,10 @@
4140
use ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface;
4241
use ApiPlatform\Laravel\Eloquent\State\PersistProcessor;
4342
use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor;
44-
use ApiPlatform\Laravel\Exception\ErrorHandler;
4543
use ApiPlatform\Laravel\Metadata\CacheResourceCollectionMetadataFactory;
4644
use ApiPlatform\Laravel\Metadata\ParameterValidationResourceMetadataCollectionFactory;
4745
use ApiPlatform\Laravel\State\ParameterValidatorProvider;
4846
use ApiPlatform\Laravel\State\SwaggerUiProcessor;
49-
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
5047
use ApiPlatform\Metadata\InflectorInterface;
5148
use ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface;
5249
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
@@ -81,11 +78,9 @@
8178
use ApiPlatform\State\Provider\ParameterProvider;
8279
use ApiPlatform\State\Provider\SecurityParameterProvider;
8380
use ApiPlatform\State\ProviderInterface;
84-
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerInterface;
8581
use Illuminate\Contracts\Foundation\Application;
8682
use Illuminate\Contracts\Support\DeferrableProvider;
8783
use Illuminate\Support\ServiceProvider;
88-
use Negotiation\Negotiator;
8984
use Psr\Log\LoggerInterface;
9085
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
9186
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -248,25 +243,6 @@ public function register(): void
248243
);
249244
});
250245

251-
$this->app->singleton(
252-
ExceptionHandlerInterface::class,
253-
function (Application $app) {
254-
/** @var ConfigRepository */
255-
$config = $app['config'];
256-
257-
return new ErrorHandler(
258-
$app,
259-
$app->make(ResourceMetadataCollectionFactoryInterface::class),
260-
$app->make(ApiPlatformController::class),
261-
$app->make(IdentifiersExtractorInterface::class),
262-
$app->make(ResourceClassResolverInterface::class),
263-
$app->make(Negotiator::class),
264-
$config->get('api-platform.exception_to_status'),
265-
$config->get('app.debug')
266-
);
267-
}
268-
);
269-
270246
if (interface_exists(FieldsBuilderEnumInterface::class)) {
271247
$this->registerGraphQl();
272248
}

Diff for: ‎src/Laravel/ApiPlatformProvider.php

+27
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
7676
use ApiPlatform\Laravel\ApiResource\Error;
7777
use ApiPlatform\Laravel\ApiResource\ValidationError;
78+
use ApiPlatform\Laravel\Controller\ApiPlatformController;
7879
use ApiPlatform\Laravel\Controller\DocumentationController;
7980
use ApiPlatform\Laravel\Controller\EntrypointController;
8081
use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilterParameterProvider;
@@ -87,6 +88,7 @@
8788
use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor;
8889
use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder;
8990
use ApiPlatform\Laravel\Eloquent\Serializer\SnakeCaseToCamelCaseNameConverter;
91+
use ApiPlatform\Laravel\Exception\ErrorHandler;
9092
use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController;
9193
use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController;
9294
use ApiPlatform\Laravel\JsonApi\State\JsonApiProvider;
@@ -154,6 +156,7 @@
154156
use ApiPlatform\State\ProviderInterface;
155157
use ApiPlatform\State\SerializerContextBuilderInterface;
156158
use Illuminate\Config\Repository as ConfigRepository;
159+
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerInterface;
157160
use Illuminate\Contracts\Foundation\Application;
158161
use Illuminate\Routing\Router;
159162
use Illuminate\Support\ServiceProvider;
@@ -318,6 +321,10 @@ public function register(): void
318321
return new HydraPrefixNameConverter(new MetadataAwareNameConverter($app->make(ClassMetadataFactoryInterface::class), $app->make(SnakeCaseToCamelCaseNameConverter::class)), $defaultContext);
319322
});
320323

324+
$this->app->singleton(OperationMetadataFactory::class, function (Application $app) {
325+
return new OperationMetadataFactory($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ResourceMetadataCollectionFactoryInterface::class));
326+
});
327+
321328
$this->app->bind(OperationMetadataFactoryInterface::class, OperationMetadataFactory::class);
322329

323330
$this->app->singleton(ReadProvider::class, function (Application $app) {
@@ -1121,6 +1128,26 @@ private function registerGraphQl(): void
11211128
formats: $config->get('api-platform.formats')
11221129
);
11231130
});
1131+
1132+
$this->app->singleton(
1133+
ExceptionHandlerInterface::class,
1134+
function (Application $app) {
1135+
/** @var ConfigRepository */
1136+
$config = $app['config'];
1137+
1138+
return new ErrorHandler(
1139+
$app,
1140+
$app->make(ResourceMetadataCollectionFactoryInterface::class),
1141+
$app->make(ApiPlatformController::class),
1142+
$app->make(IdentifiersExtractorInterface::class),
1143+
$app->make(ResourceClassResolverInterface::class),
1144+
$app->make(Negotiator::class),
1145+
$config->get('api-platform.exception_to_status'),
1146+
$config->get('app.debug'),
1147+
$config->get('api-platform.error_formats')
1148+
);
1149+
}
1150+
);
11241151
}
11251152

11261153
/**

Diff for: ‎src/Laravel/Controller/ApiPlatformController.php

+2-4
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@
1414
namespace ApiPlatform\Laravel\Controller;
1515

1616
use ApiPlatform\Metadata\HttpOperation;
17-
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactory;
17+
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
1818
use ApiPlatform\State\ProcessorInterface;
1919
use ApiPlatform\State\ProviderInterface;
20-
use Illuminate\Foundation\Application;
2120
use Illuminate\Http\Request;
2221
use Illuminate\Routing\Controller;
2322
use Symfony\Component\HttpFoundation\Response;
@@ -29,10 +28,9 @@ class ApiPlatformController extends Controller
2928
* @param ProcessorInterface<iterable<object>|object|null, Response> $processor
3029
*/
3130
public function __construct(
32-
protected OperationMetadataFactory $operationMetadataFactory,
31+
protected OperationMetadataFactoryInterface $operationMetadataFactory,
3332
protected ProviderInterface $provider,
3433
protected ProcessorInterface $processor,
35-
protected Application $app,
3634
) {
3735
}
3836

Diff for: ‎src/Laravel/Exception/ErrorHandler.php

+84-83
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
use Illuminate\Auth\AuthenticationException;
2828
use Illuminate\Contracts\Container\Container;
2929
use Illuminate\Foundation\Exceptions\Handler as ExceptionsHandler;
30-
use Illuminate\Http\Request;
3130
use Negotiation\Negotiator;
3231
use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface;
3332
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface;
@@ -52,112 +51,114 @@ public function __construct(
5251
?Negotiator $negotiator = null,
5352
private readonly ?array $exceptionToStatus = null,
5453
private readonly ?bool $debug = false,
54+
private readonly ?array $errorFormats = null,
5555
) {
5656
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
5757
$this->negotiator = $negotiator;
58-
// calls register
5958
parent::__construct($container);
60-
$this->register();
6159
}
6260

63-
public function register(): void
61+
public function render($request, \Throwable $exception)
6462
{
65-
$this->renderable(function (\Throwable $exception, Request $request) {
66-
$apiOperation = $this->initializeOperation($request);
67-
if (!$apiOperation) {
68-
return null;
69-
}
63+
$apiOperation = $this->initializeOperation($request);
64+
65+
if (!$apiOperation) {
66+
return parent::render($request, $exception);
67+
}
7068

71-
$formats = config('api-platform.error_formats') ?? ['jsonproblem' => ['application/problem+json']];
72-
$format = $request->getRequestFormat() ?? $this->getRequestFormat($request, $formats, false);
69+
$formats = $this->errorFormats ?? ['jsonproblem' => ['application/problem+json']];
70+
$format = $request->getRequestFormat() ?? $this->getRequestFormat($request, $formats, false);
7371

74-
if ($this->resourceClassResolver->isResourceClass($exception::class)) {
75-
$resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class);
72+
if ($this->resourceClassResolver->isResourceClass($exception::class)) {
73+
$resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class);
7674

77-
$operation = null;
78-
foreach ($resourceCollection as $resource) {
79-
foreach ($resource->getOperations() as $op) {
80-
foreach ($op->getOutputFormats() as $key => $value) {
81-
if ($key === $format) {
82-
$operation = $op;
83-
break 3;
84-
}
75+
$operation = null;
76+
foreach ($resourceCollection as $resource) {
77+
foreach ($resource->getOperations() as $op) {
78+
foreach ($op->getOutputFormats() as $key => $value) {
79+
if ($key === $format) {
80+
$operation = $op;
81+
break 3;
8582
}
8683
}
8784
}
85+
}
8886

89-
// No operation found for the requested format, we take the first available
90-
if (!$operation) {
91-
$operation = $resourceCollection->getOperation();
92-
}
93-
$errorResource = $exception;
94-
if ($errorResource instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) {
95-
$statusCode = $this->getStatusCode($apiOperation, $operation, $exception);
96-
$operation = $operation->withStatus($statusCode);
97-
if ($errorResource instanceof StatusAwareExceptionInterface) {
98-
$errorResource->setStatus($statusCode);
99-
}
100-
}
101-
} else {
102-
// Create a generic, rfc7807 compatible error according to the wanted format
103-
$operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format));
104-
// status code may be overridden by the exceptionToStatus option
105-
$statusCode = 500;
106-
if ($operation instanceof HttpOperation) {
107-
$statusCode = $this->getStatusCode($apiOperation, $operation, $exception);
108-
$operation = $operation->withStatus($statusCode);
87+
// No operation found for the requested format, we take the first available
88+
if (!$operation) {
89+
$operation = $resourceCollection->getOperation();
90+
}
91+
$errorResource = $exception;
92+
if ($errorResource instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) {
93+
$statusCode = $this->getStatusCode($apiOperation, $operation, $exception);
94+
$operation = $operation->withStatus($statusCode);
95+
if ($errorResource instanceof StatusAwareExceptionInterface) {
96+
$errorResource->setStatus($statusCode);
10997
}
110-
111-
$errorResource = Error::createFromException($exception, $statusCode);
11298
}
113-
114-
/** @var HttpOperation $operation */
115-
if (!$operation->getProvider()) {
116-
static::$error = $errorResource;
117-
$operation = $operation->withProvider([self::class, 'provide']);
99+
} else {
100+
// Create a generic, rfc7807 compatible error according to the wanted format
101+
$operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format));
102+
// status code may be overridden by the exceptionToStatus option
103+
$statusCode = 500;
104+
if ($operation instanceof HttpOperation) {
105+
$statusCode = $this->getStatusCode($apiOperation, $operation, $exception);
106+
$operation = $operation->withStatus($statusCode);
118107
}
119108

120-
// For our swagger Ui errors
121-
if ('html' === $format) {
122-
$operation = $operation->withOutputFormats(['html' => ['text/html']]);
123-
}
109+
$errorResource = Error::createFromException($exception, $statusCode);
110+
}
124111

125-
$identifiers = [];
126-
try {
127-
$identifiers = $this->identifiersExtractor?->getIdentifiersFromItem($errorResource, $operation) ?? [];
128-
} catch (\Exception $e) {
129-
}
112+
/** @var HttpOperation $operation */
113+
if (!$operation->getProvider()) {
114+
static::$error = $errorResource;
115+
$operation = $operation->withProvider([self::class, 'provide']);
116+
}
130117

131-
$normalizationContext = $operation->getNormalizationContext() ?? [];
132-
if (!($normalizationContext['api_error_resource'] ?? false)) {
133-
$normalizationContext += ['api_error_resource' => true];
134-
}
118+
// For our swagger Ui errors
119+
if ('html' === $format) {
120+
$operation = $operation->withOutputFormats(['html' => ['text/html']]);
121+
}
135122

136-
if (!isset($normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES])) {
137-
$normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES] = true === $this->debug ? [] : ['originalTrace'];
138-
}
123+
$identifiers = [];
124+
try {
125+
$identifiers = $this->identifiersExtractor?->getIdentifiersFromItem($errorResource, $operation) ?? [];
126+
} catch (\Exception $e) {
127+
}
139128

140-
$operation = $operation->withNormalizationContext($normalizationContext);
141-
142-
$dup = $request->duplicate(null, null, []);
143-
$dup->setMethod('GET');
144-
$dup->attributes->set('_api_resource_class', $operation->getClass());
145-
$dup->attributes->set('_api_previous_operation', $apiOperation);
146-
$dup->attributes->set('_api_operation', $operation);
147-
$dup->attributes->set('_api_operation_name', $operation->getName());
148-
$dup->attributes->set('exception', $exception);
149-
// These are for swagger
150-
$dup->attributes->set('_api_original_route', $request->attributes->get('_route'));
151-
$dup->attributes->set('_api_original_uri_variables', $request->attributes->get('_api_uri_variables'));
152-
$dup->attributes->set('_api_original_route_params', $request->attributes->get('_route_params'));
153-
$dup->attributes->set('_api_requested_operation', $request->attributes->get('_api_requested_operation'));
154-
155-
foreach ($identifiers as $name => $value) {
156-
$dup->attributes->set($name, $value);
157-
}
129+
$normalizationContext = $operation->getNormalizationContext() ?? [];
130+
if (!($normalizationContext['api_error_resource'] ?? false)) {
131+
$normalizationContext += ['api_error_resource' => true];
132+
}
133+
134+
if (!isset($normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES])) {
135+
$normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES] = true === $this->debug ? [] : ['originalTrace'];
136+
}
158137

138+
$operation = $operation->withNormalizationContext($normalizationContext);
139+
140+
$dup = $request->duplicate(null, null, []);
141+
$dup->setMethod('GET');
142+
$dup->attributes->set('_api_resource_class', $operation->getClass());
143+
$dup->attributes->set('_api_previous_operation', $apiOperation);
144+
$dup->attributes->set('_api_operation', $operation);
145+
$dup->attributes->set('_api_operation_name', $operation->getName());
146+
$dup->attributes->set('exception', $exception);
147+
// These are for swagger
148+
$dup->attributes->set('_api_original_route', $request->attributes->get('_route'));
149+
$dup->attributes->set('_api_original_uri_variables', $request->attributes->get('_api_uri_variables'));
150+
$dup->attributes->set('_api_original_route_params', $request->attributes->get('_route_params'));
151+
$dup->attributes->set('_api_requested_operation', $request->attributes->get('_api_requested_operation'));
152+
153+
foreach ($identifiers as $name => $value) {
154+
$dup->attributes->set($name, $value);
155+
}
156+
157+
try {
159158
return $this->apiPlatformController->__invoke($dup);
160-
});
159+
} catch (\Throwable $e) {
160+
return parent::render($dup, $e);
161+
}
161162
}
162163

163164
private function getStatusCode(?HttpOperation $apiOperation, ?HttpOperation $errorOperation, \Throwable $exception): int

0 commit comments

Comments
 (0)
Please sign in to comment.