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