Skip to content

Commit

Permalink
fix: errors without compatibility flag (#5841)
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Sep 20, 2023
1 parent 4ac62b0 commit 7ebff27
Show file tree
Hide file tree
Showing 20 changed files with 142 additions and 56 deletions.
45 changes: 40 additions & 5 deletions features/hydra/error.feature
Expand Up @@ -42,13 +42,12 @@ Feature: Error handling
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
And the JSON node "hydra:description" should be equal to 'Nested documents for attribute "relatedDummy" are not allowed. Use IRIs instead.'
And the JSON node "trace" should exist
And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'

Scenario: Get an error during deserialization of collection
When I add "Content-Type" header equal to "application/ld+json"
Expand All @@ -63,7 +62,7 @@ Feature: Error handling
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand All @@ -80,7 +79,7 @@ Feature: Error handling
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand All @@ -98,7 +97,7 @@ Feature: Error handling
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand Down Expand Up @@ -152,3 +151,39 @@ Feature: Error handling
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"

Scenario: Get an rfc 7807 validation error
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/validation_exception_problems" with body:
"""
{}
"""
Then the response status code should be 422
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
And the JSON node "@context" should not exist

Scenario: Get an rfc 7807 error
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/exception_problems" with body:
"""
{}
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
And the JSON node "@context" should not exist

Scenario: Get an rfc 7807 error with backward compatibility
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/exception_problems_with_compatibility" with body:
"""
{}
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the header "Link" should not contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
And the JSON node "@context" should exist
2 changes: 1 addition & 1 deletion features/main/not_exposed.feature
Expand Up @@ -171,7 +171,7 @@ Feature: Expose only a collection of objects
When I send a "GET" request to "<uri>"
Then the response status code should be 404
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "hydra:description" should be equal to "<hydra:description>"
Examples:
| uri | hydra:description |
Expand Down
6 changes: 1 addition & 5 deletions features/main/relation.feature
Expand Up @@ -484,15 +484,12 @@ Feature: Relations support
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
And the JSON should be valid according to this schema:
"""
{
"type": "object",
"properties": {
"@context": {
"type": "string",
"pattern": "^/contexts/Error$"
},
"@type": {
"type": "string",
"pattern": "^hydra:Error$"
Expand All @@ -506,7 +503,6 @@ Feature: Relations support
}
},
"required": [
"@context",
"@type",
"hydra:title",
"hydra:description"
Expand Down
4 changes: 2 additions & 2 deletions features/mongodb/filters.feature
Expand Up @@ -10,7 +10,7 @@ Feature: Filters on collections
When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.badFourthLevel.level=4"
Then the response status code should be 500
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand All @@ -21,7 +21,7 @@ Feature: Filters on collections
When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level=3"
Then the response status code should be 500
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand Down
1 change: 0 additions & 1 deletion features/security/send_security_headers.feature
Expand Up @@ -17,7 +17,6 @@ Feature: Send security header
{"name": 1}
"""
Then the response status code should be 400
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "X-Content-Type-Options" should be equal to "nosniff"
And the header "X-Frame-Options" should be equal to "deny"

Expand Down
12 changes: 6 additions & 6 deletions features/security/strong_typing.feature
Expand Up @@ -52,7 +52,7 @@ Feature: Handle properly invalid data submitted to the API
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand All @@ -69,7 +69,7 @@ Feature: Handle properly invalid data submitted to the API
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand All @@ -87,7 +87,7 @@ Feature: Handle properly invalid data submitted to the API
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"

Scenario: Send non-array data when an array is expected
When I add "Content-Type" header equal to "application/ld+json"
Expand All @@ -100,7 +100,7 @@ Feature: Handle properly invalid data submitted to the API
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand All @@ -118,7 +118,7 @@ Feature: Handle properly invalid data submitted to the API
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand All @@ -134,7 +134,7 @@ Feature: Handle properly invalid data submitted to the API
"""
Then the response status code should be 400
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context" should be equal to "/contexts/Error"
And the JSON node "@type" should be equal to "hydra:Error"
And the JSON node "hydra:title" should be equal to "An error occurred"
Expand Down
2 changes: 1 addition & 1 deletion features/security/validate_incoming_content-types.feature
Expand Up @@ -13,5 +13,5 @@ Feature: Validate incoming content type
something
"""
Then the response status code should be 415
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "hydra:description" should be equal to 'The content-type "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html", "application/graphql", "multipart/form-data".'
6 changes: 1 addition & 5 deletions features/serializer/vo_relations.feature
Expand Up @@ -140,15 +140,12 @@ Feature: Value object as ApiResource
"""
Then the response status code should be 400
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the header "Link" should contain '<http://www.w3.org/ns/hydra/error>; rel="http://www.w3.org/ns/json-ld#error"'
And the JSON should be valid according to this schema:
"""
{
"type": "object",
"properties": {
"@context": {
"type": "string",
"pattern": "^/contexts/Error$"
},
"@type": {
"type": "string",
"pattern": "^hydra:Error$"
Expand All @@ -162,7 +159,6 @@ Feature: Value object as ApiResource
}
},
"required": [
"@context",
"@type",
"hydra:title",
"hydra:description"
Expand Down
4 changes: 2 additions & 2 deletions src/Hydra/EventListener/AddLinkHeaderListener.php
Expand Up @@ -51,7 +51,7 @@ public function onKernelResponse(ResponseEvent $event): void

$apiDocUrl = $this->urlGenerator->generate('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL);
$apiDocLink = new Link(ContextBuilder::HYDRA_NS.'apiDocumentation', $apiDocUrl);
$linkProvider = $request->attributes->get('_links', new GenericLinkProvider());
$linkProvider = $request->attributes->get('_api_platform_links', new GenericLinkProvider());

if (!$linkProvider instanceof EvolvableLinkProviderInterface) {
return;
Expand All @@ -63,6 +63,6 @@ public function onKernelResponse(ResponseEvent $event): void
}
}

$request->attributes->set('_links', $linkProvider->withLink($apiDocLink));
$request->attributes->set('_api_platform_links', $linkProvider->withLink($apiDocLink));
}
}
4 changes: 2 additions & 2 deletions src/Hydra/State/HydraLinkProcessor.php
Expand Up @@ -40,14 +40,14 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
}

$apiDocUrl = $this->urlGenerator->generate('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL);
$linkProvider = $request->attributes->get('_links') ?? new GenericLinkProvider();
$linkProvider = $request->attributes->get('_api_platform_links') ?? new GenericLinkProvider();

foreach ($operation->getLinks() ?? [] as $link) {
$linkProvider = $linkProvider->withLink($link);
}

$link = new Link(ContextBuilder::HYDRA_NS.'apiDocumentation', $apiDocUrl);
$request->attributes->set('_links', $linkProvider->withLink($link));
$request->attributes->set('_api_platform_links', $linkProvider->withLink($link));

return $this->decorated->process($data, $operation, $uriVariables, $context);
}
Expand Down
5 changes: 5 additions & 0 deletions src/JsonLd/Serializer/JsonLdContextTrait.php
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
use ApiPlatform\JsonLd\ContextBuilderInterface;
use ApiPlatform\Metadata\Error;

/**
* Creates and manipulates the Serializer context.
Expand Down Expand Up @@ -42,6 +43,10 @@ private function addJsonLdContext(ContextBuilderInterface $contextBuilder, strin
return $data;
}

if (($operation = $context['operation'] ?? null) && ($operation->getExtraProperties()['rfc_7807_compliant_errors'] ?? false) && $operation instanceof Error) {
return $data;
}

$data['@context'] = $contextBuilder->getResourceContextUri($resourceClass);

return $data;
Expand Down
4 changes: 1 addition & 3 deletions src/State/Processor/AddLinkHeaderProcessor.php
Expand Up @@ -36,11 +36,9 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
}

// We add our header here as Symfony does it only for the main Request and we want it to be done on errors (sub-request) as well
$linksProvider = $request->attributes->get('_links');
$linksProvider = $request->attributes->get('_api_platform_links');
if ($this->serializer && ($links = $linksProvider->getLinks())) {
$response->headers->set('Link', $this->serializer->serialize($links));
// We don't want Symfony WebLink component do add links twice
$request->attributes->set('_links', []);
}

return $response;
Expand Down
16 changes: 14 additions & 2 deletions src/Symfony/EventListener/AddLinkHeaderListener.php
Expand Up @@ -17,8 +17,10 @@
use ApiPlatform\State\Util\CorsTrait;
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
use ApiPlatform\Symfony\Util\RequestAttributesExtractor;
use Psr\Link\LinkProviderInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\Mercure\Discovery;
use Symfony\Component\WebLink\HttpHeaderSerializer;

/**
* Adds the HTTP Link header pointing to the Mercure hub for resources having their updates dispatched.
Expand All @@ -30,8 +32,11 @@ final class AddLinkHeaderListener
use CorsTrait;
use OperationRequestInitiatorTrait;

public function __construct(private readonly Discovery $discovery, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null)
{
public function __construct(
private readonly Discovery $discovery,
ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
private readonly HttpHeaderSerializer $serializer = new HttpHeaderSerializer()
) {
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
}

Expand All @@ -48,6 +53,13 @@ public function onKernelResponse(ResponseEvent $event): void
return;
}

// Does the same as the web-link AddLinkHeaderListener as we want to use `_api_platform_links` not `_links`,
// note that the AddLinkHeaderProcessor is doing it with the MainController
$linkProvider = $event->getRequest()->attributes->get('_api_platform_links');
if ($operation && $linkProvider instanceof LinkProviderInterface && $links = $linkProvider->getLinks()) {
$event->getResponse()->headers->set('Link', $this->serializer->serialize($links), false);
}

if (
null === $request->attributes->get('_api_resource_class')
|| !($attributes = RequestAttributesExtractor::extractAttributes($request))
Expand Down
21 changes: 12 additions & 9 deletions src/Symfony/EventListener/ErrorListener.php
Expand Up @@ -18,7 +18,6 @@
use ApiPlatform\Metadata\Error as ErrorOperation;
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\Util\ContentNegotiationTrait;
Expand Down Expand Up @@ -107,7 +106,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re
}
} else {
/** @var HttpOperation $operation */
$operation = new ErrorOperation(name: '_api_errors_problem', class: Error::class, outputFormats: ['jsonld' => ['application/ld+json']], normalizationContext: ['groups' => ['jsonld'], 'skip_null_values' => true]);
$operation = new ErrorOperation(name: '_api_errors_problem', class: Error::class, outputFormats: ['jsonld' => ['application/problem+json']], normalizationContext: ['groups' => ['jsonld'], 'skip_null_values' => true]);
$operation = $operation->withStatus($this->getStatusCode($apiOperation, $request, $operation, $exception));
$errorResource = Error::createFromException($exception, $operation->getStatus());
}
Expand All @@ -128,13 +127,17 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re
} catch (\Exception $e) {
}

if ($exception instanceof ValidationException) {
if (!($apiOperation?->getExtraProperties()['rfc_7807_compliant_errors'] ?? false)) {
$operation = $operation->withNormalizationContext([
'groups' => ['legacy_'.$format],
'force_iri_generation' => false,
]);
}
if ($exception instanceof ValidationException && !($apiOperation?->getExtraProperties()['rfc_7807_compliant_errors'] ?? false)) {
$operation = $operation->withNormalizationContext([
'groups' => ['legacy_'.$format],
'force_iri_generation' => false,
]);
}

if ('jsonld' === $format && !($apiOperation?->getExtraProperties()['rfc_7807_compliant_errors'] ?? false)) {
$operation = $operation->withOutputFormats(['jsonld' => ['application/ld+json']])
->withLinks([])
->withExtraProperties(['rfc_7807_compliant_errors' => false] + $operation->getExtraProperties());
}

$dup->attributes->set('_api_resource_class', $operation->getClass());
Expand Down

0 comments on commit 7ebff27

Please sign in to comment.