Skip to content

Commit e041d72

Browse files
authoredFeb 7, 2025··
fix: errors retrieval and documentation (#6952)
1 parent 62377f8 commit e041d72

35 files changed

+455
-132
lines changed
 

‎features/main/exception_to_status.feature

-5
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,3 @@ Feature: Using exception_to_status config
4545
And I send a "GET" request to "/issue5924"
4646
Then the response status code should be 429
4747
Then the header "retry-after" should be equal to 32
48-
49-
Scenario: Show error page
50-
When I add "Accept" header equal to "text/html"
51-
And I send a "GET" request to "/errors/404"
52-
Then the response status code should be 200

‎src/JsonSchema/DefinitionNameFactory.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ public function create(string $className, string $format = 'json', ?string $inpu
5050
}
5151

5252
$definitionName = $serializerContext[SchemaFactory::OPENAPI_DEFINITION_NAME] ?? null;
53-
if ($definitionName) {
54-
$name = \sprintf('%s-%s', $prefix, $definitionName);
53+
if (null !== $definitionName) {
54+
$name = \sprintf('%s%s', $prefix, $definitionName ? '-'.$definitionName : $definitionName);
5555
} else {
5656
$groups = (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []);
5757
$name = $groups ? \sprintf('%s-%s', $prefix, implode('_', $groups)) : $prefix;

‎src/JsonSchema/ResourceMetadataTrait.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ private function findOperation(string $className, string $type, ?Operation $oper
5454
$operation = new HttpOperation();
5555
}
5656

57-
return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $format);
57+
return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $forceSubschema ? null : $format);
5858
}
5959

6060
// The best here is to use an Operation when calling `buildSchema`, we try to do a smart guess otherwise

‎src/JsonSchema/SchemaFactory.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function buildSchema(string $className, string $format = 'json', string $
6161
$inputOrOutputClass = $className;
6262
$serializerContext ??= [];
6363
} else {
64-
$operation = $this->findOperation($className, $type, $operation, $serializerContext);
64+
$operation = $this->findOperation($className, $type, $operation, $serializerContext, $format);
6565
$inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
6666
$serializerContext ??= $this->getSerializerContext($operation, $type);
6767
}
@@ -74,7 +74,6 @@ public function buildSchema(string $className, string $format = 'json', string $
7474
$validationGroups = $operation ? $this->getValidationGroups($operation) : [];
7575
$version = $schema->getVersion();
7676
$definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);
77-
7877
$method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET';
7978
if (!$operation) {
8079
$method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET';
@@ -291,6 +290,10 @@ private function getFactoryOptions(array $serializerContext, array $validationGr
291290
$options['validation_groups'] = $validationGroups;
292291
}
293292

293+
if ($operation && ($ignoredAttributes = $operation->getNormalizationContext()['ignored_attributes'] ?? null)) {
294+
$options['ignored_attributes'] = $ignoredAttributes;
295+
}
296+
294297
return $options;
295298
}
296299

‎src/Laravel/ApiPlatformProvider.php

+21-2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
use ApiPlatform\JsonSchema\SchemaFactory;
7878
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
7979
use ApiPlatform\Laravel\ApiResource\Error;
80+
use ApiPlatform\Laravel\ApiResource\ValidationError;
8081
use ApiPlatform\Laravel\Controller\ApiPlatformController;
8182
use ApiPlatform\Laravel\Controller\DocumentationController;
8283
use ApiPlatform\Laravel\Controller\EntrypointController;
@@ -177,6 +178,7 @@
177178
use ApiPlatform\Serializer\SerializerContextBuilder;
178179
use ApiPlatform\State\CallableProcessor;
179180
use ApiPlatform\State\CallableProvider;
181+
use ApiPlatform\State\ErrorProvider;
180182
use ApiPlatform\State\Pagination\Pagination;
181183
use ApiPlatform\State\Pagination\PaginationOptions;
182184
use ApiPlatform\State\ParameterProviderInterface;
@@ -773,6 +775,9 @@ public function register(): void
773775
licenseUrl: $config->get('api-platform.swagger_ui.license.url', ''),
774776
persistAuthorization: $config->get('api-platform.swagger_ui.persist_authorization', false),
775777
httpAuth: $config->get('api-platform.swagger_ui.http_auth', []),
778+
tags: $config->get('api-platform.openapi.tags', []),
779+
errorResourceClass: Error::class,
780+
validationErrorResourceClass: ValidationError::class
776781
);
777782
});
778783

@@ -846,7 +851,9 @@ public function register(): void
846851
null,
847852
$config->get('api-platform.formats'),
848853
$app->make(Options::class),
849-
$app->make(PaginationOptions::class), // ?PaginationOptions $paginationOptions = null,
854+
$app->make(PaginationOptions::class),
855+
null,
856+
$config->get('api-platform.error_formats'),
850857
// ?RouterInterface $router = null
851858
);
852859
});
@@ -1216,6 +1223,18 @@ private function registerGraphQl(Application $app): void
12161223
});
12171224
$app->alias(GraphQlReadProvider::class, 'api_platform.graphql.state_provider.read');
12181225

1226+
$app->singleton(ErrorProvider::class, function (Application $app) {
1227+
/** @var ConfigRepository */
1228+
$config = $app['config'];
1229+
1230+
return new ErrorProvider(
1231+
$config->get('app.debug'),
1232+
$app->make(ResourceClassResolver::class),
1233+
$app->make(ResourceMetadataCollectionFactoryInterface::class),
1234+
);
1235+
});
1236+
$app->tag([ErrorProvider::class], ProviderInterface::class);
1237+
12191238
$app->singleton(ResolverProvider::class, function (Application $app) {
12201239
$resolvers = iterator_to_array($app->tagged('api_platform.graphql.resolver'));
12211240
$taggedItemResolvers = iterator_to_array($app->tagged(QueryItemResolverInterface::class));
@@ -1314,7 +1333,7 @@ private function registerGraphQl(Application $app): void
13141333
/** @var ConfigRepository */
13151334
$config = $app['config'];
13161335

1317-
return new Executor($config->get('api-platform.graphql.introspection.enabled') ?? false, $config->get('api-platform.graphql.max_query_complexity'), $config->get('api-platform.graphql.max_query_depth'));
1336+
return new Executor($config->get('api-platform.graphql.introspection.enabled') ?? false, $config->get('api-platform.graphql.max_query_complexity') ?? 500, $config->get('api-platform.graphql.max_query_depth') ?? 200);
13181337
});
13191338

13201339
$app->singleton(GraphiQlController::class, function (Application $app) {

‎src/Laravel/ApiResource/Error.php

+41-12
Original file line numberDiff line numberDiff line change
@@ -13,52 +13,81 @@
1313

1414
namespace ApiPlatform\Laravel\ApiResource;
1515

16-
use ApiPlatform\JsonLd\ContextBuilderInterface;
16+
use ApiPlatform\JsonSchema\SchemaFactory;
1717
use ApiPlatform\Metadata\ApiProperty;
1818
use ApiPlatform\Metadata\Error as Operation;
1919
use ApiPlatform\Metadata\ErrorResource;
20+
use ApiPlatform\Metadata\ErrorResourceInterface;
2021
use ApiPlatform\Metadata\Exception\HttpExceptionInterface;
2122
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
23+
use ApiPlatform\State\ErrorProvider;
2224
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface;
2325
use Symfony\Component\Serializer\Annotation\Groups;
2426
use Symfony\Component\Serializer\Annotation\Ignore;
2527
use Symfony\Component\Serializer\Annotation\SerializedName;
2628
use Symfony\Component\WebLink\Link;
2729

2830
#[ErrorResource(
29-
types: ['hydra:Error'],
31+
uriTemplate: '/errors/{status}{._format}',
3032
openapi: false,
3133
uriVariables: ['status'],
3234
operations: [
3335
new Operation(
36+
errors: [],
3437
name: '_api_errors_problem',
35-
outputFormats: ['json' => ['application/problem+json']],
38+
routeName: '_api_errors',
39+
outputFormats: ['json' => ['application/problem+json', 'application/json']],
40+
hideHydraOperation: true,
3641
normalizationContext: [
42+
SchemaFactory::OPENAPI_DEFINITION_NAME => '',
3743
'groups' => ['jsonproblem'],
3844
'skip_null_values' => true,
45+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
3946
],
40-
uriTemplate: '/errors/{status}'
4147
),
4248
new Operation(
49+
errors: [],
4350
name: '_api_errors_hydra',
44-
outputFormats: ['jsonld' => ['application/problem+json']],
51+
routeName: '_api_errors',
52+
outputFormats: ['jsonld' => ['application/problem+json', 'application/ld+json']],
4553
normalizationContext: [
54+
SchemaFactory::OPENAPI_DEFINITION_NAME => '',
4655
'groups' => ['jsonld'],
4756
'skip_null_values' => true,
57+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
4858
],
49-
links: [new Link(rel: ContextBuilderInterface::JSONLD_NS.'error', href: 'http://www.w3.org/ns/hydra/error')],
50-
uriTemplate: '/errors/{status}.jsonld'
59+
links: [new Link(rel: 'http://www.w3.org/ns/json-ld#error', href: 'http://www.w3.org/ns/hydra/error')],
5160
),
5261
new Operation(
62+
errors: [],
5363
name: '_api_errors_jsonapi',
64+
routeName: '_api_errors',
65+
hideHydraOperation: true,
5466
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
55-
normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true],
56-
uriTemplate: '/errors/{status}.jsonapi'
67+
normalizationContext: [
68+
SchemaFactory::OPENAPI_DEFINITION_NAME => '',
69+
'disable_json_schema_serializer_groups' => false,
70+
'groups' => ['jsonapi'],
71+
'skip_null_values' => true,
72+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
73+
],
74+
),
75+
new Operation(
76+
name: '_api_errors',
77+
hideHydraOperation: true,
78+
extraProperties: ['_api_disable_swagger_provider' => true],
79+
outputFormats: ['html' => ['text/html'], 'jsonapi' => ['application/vnd.api+json'], 'jsonld' => ['application/ld+json'], 'json' => ['application/problem+json', 'application/json']],
5780
),
5881
],
59-
graphQlOperations: []
82+
outputFormats: ['jsonapi' => ['application/vnd.api+json'], 'jsonld' => ['application/ld+json'], 'json' => ['application/problem+json', 'application/json']],
83+
provider: ErrorProvider::class,
84+
graphQlOperations: [],
85+
description: 'A representation of common errors.',
6086
)]
61-
class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface
87+
#[ApiProperty(property: 'previous', hydra: false, readable: false)]
88+
#[ApiProperty(property: 'traceAsString', hydra: false, readable: false)]
89+
#[ApiProperty(property: 'string', hydra: false, readable: false)]
90+
class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface, ErrorResourceInterface
6291
{
6392
/**
6493
* @var array<int, mixed>
@@ -73,7 +102,7 @@ public function __construct(
73102
private readonly string $title,
74103
private readonly string $detail,
75104
#[ApiProperty(identifier: true)] private int $status,
76-
array $originalTrace,
105+
array $originalTrace = [],
77106
private readonly ?string $instance = null,
78107
private string $type = 'about:blank',
79108
private array $headers = [],

‎src/Laravel/ApiResource/ValidationError.php

+23-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Laravel\ApiResource;
1515

16+
use ApiPlatform\Metadata\ApiProperty;
1617
use ApiPlatform\Metadata\Error as ErrorOperation;
1718
use ApiPlatform\Metadata\ErrorResource;
1819
use ApiPlatform\Metadata\Exception\HttpExceptionInterface;
@@ -32,35 +33,40 @@
3233
uriTemplate: '/validation_errors/{id}',
3334
status: 422,
3435
openapi: false,
36+
outputFormats: ['jsonapi' => ['application/vnd.api+json'], 'jsonld' => ['application/ld+json'], 'json' => ['application/problem+json', 'application/json']],
3537
uriVariables: ['id'],
3638
shortName: 'ValidationError',
3739
operations: [
3840
new ErrorOperation(
41+
routeName: 'api_validation_errors',
3942
name: '_api_validation_errors_problem',
4043
outputFormats: ['json' => ['application/problem+json']],
41-
normalizationContext: ['groups' => ['json'],
44+
normalizationContext: [
45+
'groups' => ['json'],
4246
'skip_null_values' => true,
47+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
4348
],
44-
uriTemplate: '/validation_errors/{id}'
4549
),
4650
new ErrorOperation(
4751
name: '_api_validation_errors_hydra',
52+
routeName: 'api_validation_errors',
4853
outputFormats: ['jsonld' => ['application/problem+json']],
4954
links: [new Link(rel: 'http://www.w3.org/ns/json-ld#error', href: 'http://www.w3.org/ns/hydra/error')],
5055
normalizationContext: [
5156
'groups' => ['jsonld'],
5257
'skip_null_values' => true,
58+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
5359
],
54-
uriTemplate: '/validation_errors/{id}.jsonld'
5560
),
5661
new ErrorOperation(
5762
name: '_api_validation_errors_jsonapi',
63+
routeName: 'api_validation_errors',
5864
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
5965
normalizationContext: [
6066
'groups' => ['jsonapi'],
6167
'skip_null_values' => true,
68+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
6269
],
63-
uriTemplate: '/validation_errors/{id}.jsonapi'
6470
),
6571
],
6672
graphQlOperations: []
@@ -139,6 +145,19 @@ public function getInstance(): ?string
139145
*/
140146
#[SerializedName('violations')]
141147
#[Groups(['json', 'jsonld', 'jsonapi'])]
148+
#[ApiProperty(
149+
jsonldContext: ['@type' => 'ConstraintViolationList'],
150+
schema: [
151+
'type' => 'array',
152+
'items' => [
153+
'type' => 'object',
154+
'properties' => [
155+
'propertyPath' => ['type' => 'string', 'description' => 'The property path of the violation'],
156+
'message' => ['type' => 'string', 'description' => 'The message associated with the violation'],
157+
],
158+
],
159+
]
160+
)]
142161
public function getViolations(): array
143162
{
144163
return $this->violations;

‎src/Laravel/Exception/ErrorHandler.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,6 @@ public function register(): void
113113

114114
/** @var HttpOperation $operation */
115115
if (!$operation->getProvider()) {
116-
// TODO: validation
117-
// static::$error = 'jsonapi' === $format && $errorResource instanceof ConstraintViolationListAwareExceptionInterface ? $errorResource->getConstraintViolationList() : $errorResource;
118116
static::$error = $errorResource;
119117
$operation = $operation->withProvider([self::class, 'provide']);
120118
}
@@ -147,7 +145,7 @@ public function register(): void
147145
$dup->attributes->set('_api_previous_operation', $apiOperation);
148146
$dup->attributes->set('_api_operation', $operation);
149147
$dup->attributes->set('_api_operation_name', $operation->getName());
150-
$dup->attributes->remove('exception');
148+
$dup->attributes->set('exception', $exception);
151149
// These are for swagger
152150
$dup->attributes->set('_api_original_route', $request->attributes->get('_route'));
153151
$dup->attributes->set('_api_original_uri_variables', $request->attributes->get('_api_uri_variables'));

‎src/Laravel/Routing/IriConverter.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ private function generateRoute(object|string $resource, int $referenceType = Url
168168
}
169169

170170
try {
171-
return $this->router->generate($operation->getName(), $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType);
171+
$routeName = $operation instanceof HttpOperation ? ($operation->getRouteName() ?? $operation->getName()) : $operation->getName();
172+
173+
return $this->router->generate($routeName, $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType);
172174
} catch (RoutingExceptionInterface $e) {
173175
throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e);
174176
}

‎src/Laravel/State/SwaggerUiProvider.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5353
|| !($request = $context['request'] ?? null)
5454
|| 'html' !== $request->getRequestFormat()
5555
|| !$this->swaggerUiEnabled
56+
|| true === ($operation->getExtraProperties()['_api_disable_swagger_provider'] ?? false)
5657
) {
5758
return $this->decorated->provide($operation, $uriVariables, $context);
5859
}
@@ -64,7 +65,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
6465
// We need to call our operation provider just in case it fails
6566
// when it fails we'll get an Error, and we'll fix the status accordingly
6667
// @see features/main/content_negotiation.feature:119
67-
// DocumentationAction has no content negotiation as well we want HTML so render swagger ui
68+
// When requesting DocumentationAction or EntrypointAction with Accept: text/html we render SwaggerUi
6869
if (!$operation instanceof Error && Documentation::class !== $operation->getClass()) {
6970
$this->decorated->provide($operation, $uriVariables, $context);
7071
}

0 commit comments

Comments
 (0)
Please sign in to comment.