Skip to content

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

Open
@f1amy

Description

@f1amy

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions