core icon indicating copy to clipboard operation
core copied to clipboard

Cannot generate IRI errors when enabling `rfc_7807_compliant_errors`

Open darthf1 opened this issue 1 year ago • 1 comments

API Platform version(s) affected: 3.4.3

Description
I'm trying to enable rfc_7807_compliant_errors as an upgrade path to v4. When I do this, in my project, not only the error output changes (obviously), but I also get:

ApiPlatform\Metadata\Exception\InvalidArgumentException: Unable to generate an IRI for the item of type  ....

How to reproduce
I have the following code:

<?php

namespace App;

#[ApiResource(
    shortName: 'Organisation',
    operations: [
        new Get(
            uriTemplate: '/organisations/{id}',
            security: "is_granted('" . OrganisationVoter::ATTRIBUTE_ORGANISATION_READ . "', object)",
            provider: ItemProvider::class,
        ),
        new Get(
            uriTemplate: '/users/{userId}/organisation',
            uriVariables: [
                'userId' => new Link(
                    fromClass: UserProjection::class,
                ),
            ],
            normalizationContext: [
                'item_uri_template' => '/organisations/{id}',
                'groups' => [self::NORMALIZATION_GROUP],
            ],
            provider: UserOrganisationProvider::class,
        ),
    ],
)]
class Organisation {}

I have two test cases:

  • fetching an Organisation for a given user, and the user has access to the Organisation, it returns the Organisation
  • fetching an Organisation for a given user, and the user does not have access to an Organisation, it returns HTTP 404.

The following happens:

  • With rfc_7807_compliant_errors to false, both test cases succeed.
  • With rfc_7807_compliant_errors to true, the second test case throws: ApiPlatform\Metadata\Exception\InvalidArgumentException: Unable to generate an IRI for the item of type "App\Organisation". When I then remove the line 'item_uri_template' => '/organisations/{id}', from the operation, the test succeeds again.

Additional Context
My (partial) config:

<?php

declare(strict_types=1);

use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Validator\Exception\ValidationException;
use Doctrine\ORM\OptimisticLockException;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\Exception\ValidationFailedException;
use Symfony\Component\Serializer\Exception\ExceptionInterface;

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->extension('api_platform', [
        'defaults' => [
            'collectDenormalizationErrors' => true,
            'extra_properties' => [
                'standard_put' => true,
                'rfc_7807_compliant_errors' => true,
            ],
            'pagination_client_enabled' => true,
            'pagination_client_items_per_page' => true,
            'pagination_items_per_page' => 30,
            'pagination_maximum_items_per_page' => 100,
            'stateless' => true,
        ],
        'enable_link_security' => true,
        'exception_to_status' => [
            ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
            InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
            OptimisticLockException::class => Response::HTTP_CONFLICT,
            ValidationException::class => Response::HTTP_UNPROCESSABLE_ENTITY,
            ValidationFailedException::class => Response::HTTP_UNPROCESSABLE_ENTITY,
        ],
        'keep_legacy_inflector' => false,
        'use_symfony_listeners' => true,
        'serializer' => [
            'hydra_prefix' => false,
        ],
    ]);
};

Full stacktrace:

ApiPlatform\Metadata\Exception\InvalidArgumentException: Unable to generate an IRI for the item of type "App\Organisation"
/home/www/app/vendor/api-platform/core/src/Symfony/Routing/IriConverter.php:194
/home/www/app/vendor/api-platform/core/src/Symfony/Routing/IriConverter.php:171
/home/www/app/vendor/api-platform/core/src/JsonLd/Serializer/ItemNormalizer.php:129
/home/www/app/vendor/api-platform/core/src/JsonLd/Serializer/ErrorNormalizer.php:31
/home/www/app/vendor/symfony/serializer/Serializer.php:150
/home/www/app/vendor/symfony/serializer/Serializer.php:129
/home/www/app/vendor/api-platform/core/src/State/Processor/SerializeProcessor.php:79
/home/www/app/vendor/api-platform/core/src/Symfony/EventListener/SerializeListener.php:102
/home/www/app/vendor/symfony/event-dispatcher/Debug/WrappedListener.php:115
/home/www/app/vendor/symfony/event-dispatcher/EventDispatcher.php:206
/home/www/app/vendor/symfony/event-dispatcher/EventDispatcher.php:56
/home/www/app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:122
/home/www/app/vendor/symfony/http-kernel/HttpKernel.php:188
/home/www/app/vendor/symfony/http-kernel/HttpKernel.php:76
/home/www/app/vendor/symfony/http-kernel/EventListener/ErrorListener.php:97
/home/www/app/vendor/api-platform/core/src/Symfony/EventListener/ExceptionListener.php:50
/home/www/app/vendor/symfony/event-dispatcher/Debug/WrappedListener.php:115
/home/www/app/vendor/symfony/event-dispatcher/EventDispatcher.php:206
/home/www/app/vendor/symfony/event-dispatcher/EventDispatcher.php:56
/home/www/app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:122
/home/www/app/vendor/symfony/http-kernel/HttpKernel.php:241
/home/www/app/vendor/symfony/http-kernel/HttpKernel.php:91
/home/www/app/vendor/symfony/http-kernel/Kernel.php:182
/home/www/app/vendor/symfony/http-kernel/HttpKernelBrowser.php:63
/home/www/app/vendor/symfony/framework-bundle/KernelBrowser.php:157
/home/www/app/vendor/symfony/browser-kit/AbstractBrowser.php:369
/home/www/app/vendor/api-platform/core/src/Symfony/Bundle/Test/Client.php:115
/home/www/app/tests/Functional/App/OrganisationTest.php:202

Caused by
ApiPlatform\Metadata\Exception\RuntimeException: Not able to retrieve identifiers.

/home/www/app/vendor/api-platform/core/src/Metadata/IdentifiersExtractor.php:139
/home/www/app/vendor/api-platform/core/src/Metadata/IdentifiersExtractor.php:90
/home/www/app/vendor/api-platform/core/src/Metadata/IdentifiersExtractor.php:60
/home/www/app/vendor/api-platform/core/src/Symfony/Routing/IriConverter.php:190
/home/www/app/vendor/api-platform/core/src/Symfony/Routing/IriConverter.php:171
/home/www/app/vendor/api-platform/core/src/JsonLd/Serializer/ItemNormalizer.php:129
/home/www/app/vendor/api-platform/core/src/JsonLd/Serializer/ErrorNormalizer.php:31
/home/www/app/vendor/symfony/serializer/Serializer.php:150
/home/www/app/vendor/symfony/serializer/Serializer.php:129
/home/www/app/vendor/api-platform/core/src/State/Processor/SerializeProcessor.php:79
/home/www/app/vendor/api-platform/core/src/Symfony/EventListener/SerializeListener.php:102
/home/www/app/vendor/symfony/event-dispatcher/Debug/WrappedListener.php:115
/home/www/app/vendor/symfony/event-dispatcher/EventDispatcher.php:206
/home/www/app/vendor/symfony/event-dispatcher/EventDispatcher.php:56
/home/www/app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:122
/home/www/app/vendor/symfony/http-kernel/HttpKernel.php:188
/home/www/app/vendor/symfony/http-kernel/HttpKernel.php:76
/home/www/app/vendor/symfony/http-kernel/EventListener/ErrorListener.php:97
/home/www/app/vendor/api-platform/core/src/Symfony/EventListener/ExceptionListener.php:50
/home/www/app/vendor/symfony/event-dispatcher/Debug/WrappedListener.php:115
/home/www/app/vendor/symfony/event-dispatcher/EventDispatcher.php:206
/home/www/app/vendor/symfony/event-dispatcher/EventDispatcher.php:56
/home/www/app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:122
/home/www/app/vendor/symfony/http-kernel/HttpKernel.php:241
/home/www/app/vendor/symfony/http-kernel/HttpKernel.php:91
/home/www/app/vendor/symfony/http-kernel/Kernel.php:182
/home/www/app/vendor/symfony/http-kernel/HttpKernelBrowser.php:63
/home/www/app/vendor/symfony/framework-bundle/KernelBrowser.php:157
/home/www/app/vendor/symfony/browser-kit/AbstractBrowser.php:369
/home/www/app/vendor/api-platform/core/src/Symfony/Bundle/Test/Client.php:115
/home/www/app/tests/Functional/App/OrganisationTest.php:202

Caused by
Symfony\Component\HttpKernel\Exception\NotFoundHttpException: Not Found

/home/www/app/vendor/api-platform/core/src/State/Provider/ReadProvider.php:94
/home/www/app/vendor/api-platform/core/src/Symfony/Validator/State/ParameterValidatorProvider.php:87
/home/www/app/vendor/api-platform/core/src/State/Provider/ParameterProvider.php:103
/home/www/app/vendor/api-platform/core/src/Symfony/Bundle/SwaggerUi/SwaggerUiProvider.php:50
/home/www/app/vendor/api-platform/core/src/Symfony/Security/State/AccessCheckerProvider.php:62
/home/www/app/vendor/api-platform/core/src/State/Provider/SecurityParameterProvider.php:39
/home/www/app/vendor/api-platform/core/src/Symfony/Security/State/LinkedReadProvider.php:42
/home/www/app/vendor/api-platform/core/src/Symfony/Security/State/LinkAccessCheckerProvider.php:40
/home/www/app/vendor/api-platform/core/src/Symfony/EventListener/ReadListener.php:95
/home/www/app/vendor/symfony/event-dispatcher/Debug/WrappedListener.php:115
/home/www/app/vendor/symfony/event-dispatcher/EventDispatcher.php:206
/home/www/app/vendor/symfony/event-dispatcher/EventDispatcher.php:56
/home/www/app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:122
/home/www/app/vendor/symfony/http-kernel/HttpKernel.php:159
/home/www/app/vendor/symfony/http-kernel/HttpKernel.php:76
/home/www/app/vendor/symfony/http-kernel/Kernel.php:182
/home/www/app/vendor/symfony/http-kernel/HttpKernelBrowser.php:63
/home/www/app/vendor/symfony/framework-bundle/KernelBrowser.php:157
/home/www/app/vendor/symfony/browser-kit/AbstractBrowser.php:369
/home/www/app/vendor/api-platform/core/src/Symfony/Bundle/Test/Client.php:115
/home/www/app/tests/Functional/App/OrganisationTest.php:202

darthf1 avatar Oct 13 '24 11:10 darthf1

Hi @darthf1 API Platform was not able to gather identifiers from Organization, can you maybe api-resource:debug the first operation and check if it has uriVariables? I have no explanation for why the flag has this impact, what I usually do to debug exceptions is to dump directly inside the ErrorListener, my guess is that an exception is thrown no matter the rfc_7807_compliant_errors flag.

soyuka avatar Oct 14 '24 07:10 soyuka

What I found so far (I put some breakpoints in my app):

  • This line is called https://github.com/api-platform/core/blob/main/src/State/Provider/ReadProvider.php#L93, because $data is null
  • Then I end up at https://github.com/symfony/http-kernel/blob/7.1/HttpKernel.php#L241
  • When I set rfc_7807_compliant_errors to false, and i press "Step Over", I go to the next line and a few clicks later it responds with a 404.
  • When I set rfc_7807_compliant_errors to true, and i press "Step Over", I somehow end up again in the ReadProvider, but this time $data is not null but an Error resource.

image

  • Then I'm here https://github.com/api-platform/core/blob/main/src/Symfony/Routing/IriConverter.php#L187
  • And then, ultimately, I end up here https://github.com/api-platform/core/blob/main/src/Symfony/Routing/IriConverter.php#L191. Reason, the item uri template expects an id uri variable, but there is none provided in the context (maybe because there was no resource found initially?):

image image

darthf1 avatar Nov 14 '24 16:11 darthf1

but you stil lget a 404? the error at:

image

Looks just fine to me

soyuka avatar Nov 15 '24 10:11 soyuka

Looks just fine to me

Yes absolutely! But that error is not handled gracefully anymore.

My tests does:

    public function testGetOwnOrganisationWhenNotMemberOfOrganisationThrowsHttpNotFound(): void
    {
        $referenceRepository = self::loadFixtures([
            UserTestFixture::class,
            OrganisationTestFixture::class,
        ]);
        $user3 = $referenceRepository->getReference(UserTestFixture::USER_3, User::class);

        $client = self::createAuthenticatedClient('[email protected]', 'user_3');
        $client->request('GET', \sprintf('/v1/users/%s/organisation', $user3->getId()));
        self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
    }
  • With rfc_7807_compliant_errors=false this tests succeeds
  • With rfc_7807_compliant_errors=true this test fails, and throws
ApiPlatform\Metadata\Exception\InvalidArgumentException: Unable to generate an IRI for the item of type "UserInterface\Organisation\Rest\OrganisationProjection"
  • With rfc_7807_compliant_errors=true and 'item_uri_template' => '/organisations/{id}', removed from the operations normalizationContext, this test succeeds as well.

darthf1 avatar Nov 15 '24 12:11 darthf1

May you provide a reproducer please?

soyuka avatar Nov 18 '24 14:11 soyuka

Quite hard to track, got it!

https://github.com/api-platform/core/pull/6816 Thanks for the bug report!

soyuka avatar Nov 22 '24 10:11 soyuka

Quite hard to track, got it!

https://github.com/api-platform/core/pull/6816 Thanks for the bug report!

That is great! Thanks a lot.

darthf1 avatar Nov 22 '24 10:11 darthf1