api-platform / docs

API Platform documentation
https://api-platform.com/docs/
164 stars 1.06k forks source link

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

Open f1amy opened 9 months ago

f1amy commented 9 months ago

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']
  2. 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);
}

}


3. 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'


4. 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**  
<!--- Optional: only if you have suggestions on a fix/reason for the bug. -->
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**  
<!-- Optional: any other context about the problem: log messages, screenshots, etc. -->
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 commented 9 months ago

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.

f1amy commented 9 months ago

@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 commented 9 months ago

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