Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot use decorated DeserializeListener in v3.2 to accept application/x-www-form-urlencoded form data #1856

Open
f1amy opened this issue Dec 13, 2023 · 7 comments

Comments

@f1amy
Copy link

f1amy commented Dec 13, 2023

API Platform version(s) affected: 3.2.7

Description

After upgrading to v3.2 and switching event_listeners_backward_compatibility_layer to false, the use of decorated DeserializeListener to support application/x-www-form-urlencoded we relied on stopped working.
We followed the current actual guide on https://api-platform.com/docs/core/form-data/ to know if there is a fix, but it seems the guide has not been updated for 3.2.

How to reproduce

  1. Use the following config:
    config/packages/api_platform.yaml
api_platform:
    title: 'API'
    version: 1.0.0

    defaults:
        stateless: true
        cache_headers:
            vary: ['Content-Type', 'Authorization', 'Origin']
        extra_properties:
            standard_put: true
            rfc_7807_compliant_errors: true
        normalization_context:
            skip_null_values: false

    event_listeners_backward_compatibility_layer: false
    keep_legacy_inflector: false

    formats:
        jsonld: ['application/ld+json']
        jsonhal: ['application/hal+json']
        jsonapi: ['application/vnd.api+json']
        json: ['application/json']

    docs_formats:
        jsonld: ['application/ld+json']
        jsonopenapi: ['application/vnd.openapi+json']
        html: ['text/html']

    error_formats:
        jsonproblem: ['application/problem+json']
        jsonld: ['application/ld+json']
        jsonapi: ['application/vnd.api+json']
  1. Add the following listener:
    \App\Infrastructure\EventListener\ApiPlatform\DeserializeListener
<?php

namespace App\Infrastructure\EventListener\ApiPlatform;

use ApiPlatform\Serializer\SerializerContextBuilderInterface;
use ApiPlatform\Symfony\EventListener\DeserializeListener as DecoratedListener;
use ApiPlatform\Symfony\Util\RequestAttributesExtractor;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

#[AsDecorator('api_platform.listener.request.deserialize')]
#[AutoconfigureTag(name: 'kernel.event_listener', attributes: ['event' => 'kernel.request', 'method' => 'onKernelRequest', 'priority' => 2])]
class DeserializeListener
{
    private DecoratedListener $decorated;
    private DenormalizerInterface $denormalizer;
    private SerializerContextBuilderInterface $serializerContextBuilder;

    public function __construct(DenormalizerInterface $denormalizer, SerializerContextBuilderInterface $serializerContextBuilder, DecoratedListener $decorated)
    {
        $this->denormalizer = $denormalizer;
        $this->serializerContextBuilder = $serializerContextBuilder;
        $this->decorated = $decorated;
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();
        if ($request->isMethodCacheable() || $request->isMethod(Request::METHOD_DELETE)) {
            return;
        }

        if ('form' === $request->getContentTypeFormat()) {
            $this->denormalizeFormRequest($request);
        } else {
            $this->decorated->onKernelRequest($event);
        }
    }

    private function denormalizeFormRequest(Request $request): void
    {
        if (!$attributes = RequestAttributesExtractor::extractAttributes($request)) {
            return;
        }

        $context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
        $populated = $request->attributes->get('data');
        if (null !== $populated) {
            $context['object_to_populate'] = $populated;
        }

        $data = $request->request->all();
        $object = $this->denormalizer->denormalize($data, $attributes['resource_class'], null, $context);
        $request->attributes->set('data', $object);
    }
}
  1. The request cURL:
curl -X POST --location "https://localhost/api/something" \
    -H "accept: application/ld+json" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d 'leads%5Bstatus%5D%5B0%5D%5Bid%5D=string&leads%5Bstatus%5D%5B0%5D%5Bstatus_id%5D=string&leads%5Bstatus%5D%5B0%5D%5Bold_status_id%5D=string&leads%5Bstatus%5D%5B0%5D%5Bpipeline_id%5D=string'
  1. Actual response:
HTTP/1.1 415 Unsupported Media Type
{
  "@id": "\/api\/errors\/415",
  "@type": "hydra:Error",
  "title": "An error occurred",
  "detail": "The content-type \"application\/x-www-form-urlencoded\" is not supported. Supported MIME types are \"application\/ld+json\", \"application\/hal+json\", \"application\/vnd.api+json\", \"application\/json\".",
  "status": 415,
  "type": "\/errors\/415",
  "trace": [
    {
      "file": "\/srv\/app\/vendor\/api-platform\/core\/src\/State\/Provider\/ContentNegotiationProvider.php",
      "line": 48,
      "function": "getInputFormat",
      "class": "ApiPlatform\\State\\Provider\\ContentNegotiationProvider",
      "type": "->"
    },
    {
      "file": "\/srv\/app\/vendor\/api-platform\/core\/src\/Symfony\/Controller\/MainController.php",
      "line": 82,
      "function": "provide",
      "class": "ApiPlatform\\State\\Provider\\ContentNegotiationProvider",
      "type": "->"
    },
    {
      "file": "\/srv\/app\/vendor\/symfony\/http-kernel\/HttpKernel.php",
      "line": 181,
      "function": "__invoke",
      "class": "ApiPlatform\\Symfony\\Controller\\MainController",
      "type": "->"
    },
    {
      "file": "\/srv\/app\/vendor\/symfony\/http-kernel\/HttpKernel.php",
      "line": 76,
      "function": "handleRaw",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->"
    },
    {
      "file": "\/srv\/app\/vendor\/symfony\/http-kernel\/Kernel.php",
      "line": 197,
      "function": "handle",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->"
    },
    {
      "file": "\/srv\/app\/vendor\/symfony\/runtime\/Runner\/Symfony\/HttpKernelRunner.php",
      "line": 35,
      "function": "handle",
      "class": "Symfony\\Component\\HttpKernel\\Kernel",
      "type": "->"
    },
    {
      "file": "\/srv\/app\/vendor\/autoload_runtime.php",
      "line": 29,
      "function": "run",
      "class": "Symfony\\Component\\Runtime\\Runner\\Symfony\\HttpKernelRunner",
      "type": "->"
    },
    {
      "file": "\/srv\/app\/public\/index.php",
      "line": 5,
      "function": "require_once"
    }
  ],
  "hydra:title": "An error occurred",
  "hydra:description": "The content-type \"application\/x-www-form-urlencoded\" is not supported. Supported MIME types are \"application\/ld+json\", \"application\/hal+json\", \"application\/vnd.api+json\", \"application\/json\"."
}

The expected response: no error.
With event_listeners_backward_compatibility_layer: true it works as expected.

Possible Solution

Have a way to ignore content negotiation mismatch error or a new way to support application/x-www-form-urlencoded with event_listeners_backward_compatibility_layer: false.

Additional Context

Notably the decorated DeserializeListener gets called with event_listeners_backward_compatibility_layer: false (didn't expect that).

Not sure if it is a bug, documentation issue, or both.

@soyuka
Copy link
Member

soyuka commented Dec 13, 2023

use event_listeners_backward_compatibility_layer: true please not that the name is quite misleading event listeners will always be supported. You can decorate our processors in 3.2 if you don't want to use listeners or just keep it like that.

@soyuka soyuka closed this as completed Dec 13, 2023
@f1amy
Copy link
Author

f1amy commented Dec 13, 2023

@soyuka, Oh, did not know that. But still, can we fix the documentation at https://api-platform.com/docs/core/form-data/ to note that it is only working with event_listeners_backward_compatibility_layer: true, which will be false by default in 4.0?

@soyuka
Copy link
Member

soyuka commented Dec 13, 2023

Yes definitely, actually we should provide a documentation on how to do this with processors! I'll open an issue there thanks!

@soyuka soyuka transferred this issue from api-platform/core Dec 13, 2023
@soyuka soyuka reopened this Dec 13, 2023
@mocxi
Copy link

mocxi commented Nov 7, 2024

Hello,
I'm using version 4.0 and there is no option: event_listeners_backward_compatibility_layer,
when I apply the decorating following the document, it says no such service: api_platform.listener.request.deserialize
I checked the code and saw this: https://github.com/api-platform/symfony/blob/main/Bundle/DependencyInjection/ApiPlatformExtension.php#L198

so if I enable use_symfony_listeners I can't use provider and processor anymore?

@vinceAmstoutz
Copy link
Contributor

vinceAmstoutz commented Nov 14, 2024

Hello, I'm using version 4.0 and there is no option: event_listeners_backward_compatibility_layer, when I apply the decorating following the document, it says no such service: api_platform.listener.request.deserialize I checked the code and saw this: https://github.com/api-platform/symfony/blob/main/Bundle/DependencyInjection/ApiPlatformExtension.php#L198

so if I enable use_symfony_listeners I can't use provider and processor anymore?

@mocxi The use of Symfony event listeners to extend API Platform is no longer recommended, as you can see in the documentation
image.

Have you tried decorating the DeserializeProvider?

@f1amy
Copy link
Author

f1amy commented Nov 14, 2024

I guess I was tagged by mistake, the above message is addressed to @mocxi

@f1amy
Copy link
Author

f1amy commented Nov 14, 2024

The current state of the issue is that there is no documentation on how to do https://api-platform.com/docs/core/form-data/ with processors instead of symfony events, which are not recommended anymore

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants