Skip to content

Commit b1590b8

Browse files
committed
Fix handling of empty request content by allowing to disable listeners per operation
The toggleable listeners are: - ReadListener ("read") - DeserializeListener ("deserialize") - ValidateListener ("validate") - WriteListener ("write") - SerializeListener ("serialize")
1 parent 9222ded commit b1590b8

File tree

16 files changed

+381
-166
lines changed

16 files changed

+381
-166
lines changed

features/main/crud.feature

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ Feature: Create-Retrieve-Update-Delete
9191
}
9292
"""
9393

94+
Scenario: Create a resource with empty body
95+
When I add "Content-Type" header equal to "application/ld+json"
96+
And I send a "POST" request to "/dummies"
97+
Then the response status code should be 400
98+
And the JSON node "hydra:description" should be equal to "Syntax error"
99+
94100
Scenario: Get a not found exception
95101
When I send a "GET" request to "/dummies/42"
96102
Then the response status code should be 404
@@ -526,42 +532,8 @@ Feature: Create-Retrieve-Update-Delete
526532
Scenario: Update a resource with empty body
527533
When I add "Content-Type" header equal to "application/ld+json"
528534
And I send a "PUT" request to "/dummies/1"
529-
Then the response status code should be 200
530-
And the response should be in JSON
531-
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
532-
And the header "Content-Location" should be equal to "/dummies/1"
533-
And the JSON should be equal to:
534-
"""
535-
{
536-
"@context": "/contexts/Dummy",
537-
"@id": "/dummies/1",
538-
"@type": "Dummy",
539-
"description": null,
540-
"dummy": null,
541-
"dummyBoolean": null,
542-
"dummyDate": "2018-12-01T13:12:00+00:00",
543-
"dummyFloat": null,
544-
"dummyPrice": null,
545-
"relatedDummy": null,
546-
"relatedDummies": [],
547-
"jsonData": [
548-
{
549-
"key": "value1"
550-
},
551-
{
552-
"key": "value2"
553-
}
554-
],
555-
"arrayData": [],
556-
"name_converted": null,
557-
"relatedOwnedDummy": null,
558-
"relatedOwningDummy": null,
559-
"id": 1,
560-
"name": "A nice dummy",
561-
"alias": null,
562-
"foo": null
563-
}
564-
"""
535+
Then the response status code should be 400
536+
And the JSON node "hydra:description" should be equal to "Syntax error"
565537

566538
Scenario: Delete a resource
567539
When I send a "DELETE" request to "/dummies/1"

src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
3333
use Symfony\Component\HttpFoundation\Response;
3434
use Symfony\Component\Messenger\MessageBusInterface;
35-
use Symfony\Component\Serializer\Exception\ExceptionInterface;
35+
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface;
3636

3737
/**
3838
* The configuration of the bundle.
@@ -369,7 +369,7 @@ private function addExceptionToStatusSection(ArrayNodeDefinition $rootNode): voi
369369
->children()
370370
->arrayNode('exception_to_status')
371371
->defaultValue([
372-
ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
372+
SerializerExceptionInterface::class => Response::HTTP_BAD_REQUEST,
373373
InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
374374
FilterValidationException::class => Response::HTTP_BAD_REQUEST,
375375
OptimisticLockException::class => Response::HTTP_CONFLICT,

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,15 @@
154154
<argument type="service" id="api_platform.subresource_data_provider" />
155155
<argument type="service" id="api_platform.serializer.context_builder" />
156156
<argument type="service" id="api_platform.identifier.converter" />
157+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
157158

158159
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="4" />
159160
</service>
160161

161162
<service id="api_platform.listener.view.write" class="ApiPlatform\Core\EventListener\WriteListener">
162163
<argument type="service" id="api_platform.data_persister" />
163164
<argument type="service" id="api_platform.iri_converter" on-invalid="null" />
164-
<argument type="service" id="api_platform.metadata.resource.metadata_factory" on-invalid="null" />
165+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
165166

166167
<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="32" />
167168
</service>
@@ -170,13 +171,15 @@
170171
<argument type="service" id="api_platform.serializer" />
171172
<argument type="service" id="api_platform.serializer.context_builder" />
172173
<argument type="service" id="api_platform.formats_provider" />
174+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
173175

174176
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="2" />
175177
</service>
176178

177179
<service id="api_platform.listener.view.serialize" class="ApiPlatform\Core\EventListener\SerializeListener">
178180
<argument type="service" id="api_platform.serializer" />
179181
<argument type="service" id="api_platform.serializer.context_builder" />
182+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
180183

181184
<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="16" />
182185
</service>

src/EventListener/DeserializeListener.php

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use ApiPlatform\Core\Api\FormatMatcher;
1717
use ApiPlatform\Core\Api\FormatsProviderInterface;
1818
use ApiPlatform\Core\Exception\InvalidArgumentException;
19+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
20+
use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
1921
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
2022
use ApiPlatform\Core\Util\RequestAttributesExtractor;
2123
use Symfony\Component\HttpFoundation\Request;
@@ -31,6 +33,10 @@
3133
*/
3234
final class DeserializeListener
3335
{
36+
use ToggleableOperationAttributeTrait;
37+
38+
public const OPERATION_ATTRIBUTE_KEY = 'deserialize';
39+
3440
private $serializer;
3541
private $serializerContextBuilder;
3642
private $formats = [];
@@ -40,7 +46,7 @@ final class DeserializeListener
4046
/**
4147
* @throws InvalidArgumentException
4248
*/
43-
public function __construct(SerializerInterface $serializer, SerializerContextBuilderInterface $serializerContextBuilder, /* FormatsProviderInterface */$formatsProvider)
49+
public function __construct(SerializerInterface $serializer, SerializerContextBuilderInterface $serializerContextBuilder, /* FormatsProviderInterface */$formatsProvider, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
4450
{
4551
$this->serializer = $serializer;
4652
$this->serializerContextBuilder = $serializerContextBuilder;
@@ -54,6 +60,7 @@ public function __construct(SerializerInterface $serializer, SerializerContextBu
5460

5561
$this->formatsProvider = $formatsProvider;
5662
}
63+
$this->resourceMetadataFactory = $resourceMetadataFactory;
5764
}
5865

5966
/**
@@ -69,18 +76,12 @@ public function onKernelRequest(GetResponseEvent $event): void
6976
|| $request->isMethodSafe(false)
7077
|| !($attributes = RequestAttributesExtractor::extractAttributes($request))
7178
|| !$attributes['receive']
72-
|| (
73-
'' === ($requestContent = $request->getContent())
74-
&& ('POST' === $method || 'PUT' === $method)
75-
)
79+
|| $this->isOperationAttributeDisabled($attributes, self::OPERATION_ATTRIBUTE_KEY)
7680
) {
7781
return;
7882
}
7983

8084
$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
81-
if (isset($context['input']) && \array_key_exists('class', $context['input']) && null === $context['input']['class']) {
82-
return;
83-
}
8485

8586
// BC check to be removed in 3.0
8687
if (null !== $this->formatsProvider) {
@@ -96,9 +97,7 @@ public function onKernelRequest(GetResponseEvent $event): void
9697

9798
$request->attributes->set(
9899
'data',
99-
$this->serializer->deserialize(
100-
$requestContent, $context['resource_class'], $format, $context
101-
)
100+
$this->serializer->deserialize($request->getContent(), $context['resource_class'], $format, $context)
102101
);
103102
}
104103

src/EventListener/ReadListener.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
use ApiPlatform\Core\Exception\InvalidIdentifierException;
2121
use ApiPlatform\Core\Exception\RuntimeException;
2222
use ApiPlatform\Core\Identifier\IdentifierConverterInterface;
23+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
24+
use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
2325
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
2426
use ApiPlatform\Core\Util\RequestAttributesExtractor;
2527
use ApiPlatform\Core\Util\RequestParser;
@@ -34,16 +36,20 @@
3436
final class ReadListener
3537
{
3638
use OperationDataProviderTrait;
39+
use ToggleableOperationAttributeTrait;
40+
41+
public const OPERATION_ATTRIBUTE_KEY = 'read';
3742

3843
private $serializerContextBuilder;
3944

40-
public function __construct(CollectionDataProviderInterface $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierConverterInterface $identifierConverter = null)
45+
public function __construct(CollectionDataProviderInterface $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierConverterInterface $identifierConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
4146
{
4247
$this->collectionDataProvider = $collectionDataProvider;
4348
$this->itemDataProvider = $itemDataProvider;
4449
$this->subresourceDataProvider = $subresourceDataProvider;
4550
$this->serializerContextBuilder = $serializerContextBuilder;
4651
$this->identifierConverter = $identifierConverter;
52+
$this->resourceMetadataFactory = $resourceMetadataFactory;
4753
}
4854

4955
/**
@@ -57,6 +63,8 @@ public function onKernelRequest(GetResponseEvent $event): void
5763
if (
5864
!($attributes = RequestAttributesExtractor::extractAttributes($request))
5965
|| !$attributes['receive']
66+
|| $request->isMethod('POST') && isset($attributes['collection_operation_name'])
67+
|| $this->isOperationAttributeDisabled($attributes, self::OPERATION_ATTRIBUTE_KEY)
6068
) {
6169
return;
6270
}
@@ -74,7 +82,7 @@ public function onKernelRequest(GetResponseEvent $event): void
7482
}
7583

7684
if (isset($attributes['collection_operation_name'])) {
77-
$request->attributes->set('data', $request->isMethod('POST') ? null : $this->getCollectionData($attributes, $context));
85+
$request->attributes->set('data', $this->getCollectionData($attributes, $context));
7886

7987
return;
8088
}

src/EventListener/SerializeListener.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace ApiPlatform\Core\EventListener;
1515

1616
use ApiPlatform\Core\Exception\RuntimeException;
17+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
18+
use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
1719
use ApiPlatform\Core\Serializer\ResourceList;
1820
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
1921
use ApiPlatform\Core\Util\RequestAttributesExtractor;
@@ -32,13 +34,18 @@
3234
*/
3335
final class SerializeListener
3436
{
37+
use ToggleableOperationAttributeTrait;
38+
39+
public const OPERATION_ATTRIBUTE_KEY = 'serialize';
40+
3541
private $serializer;
3642
private $serializerContextBuilder;
3743

38-
public function __construct(SerializerInterface $serializer, SerializerContextBuilderInterface $serializerContextBuilder)
44+
public function __construct(SerializerInterface $serializer, SerializerContextBuilderInterface $serializerContextBuilder, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
3945
{
4046
$this->serializer = $serializer;
4147
$this->serializerContextBuilder = $serializerContextBuilder;
48+
$this->resourceMetadataFactory = $resourceMetadataFactory;
4249
}
4350

4451
/**
@@ -49,7 +56,11 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
4956
$controllerResult = $event->getControllerResult();
5057
$request = $event->getRequest();
5158

52-
if ($controllerResult instanceof Response || !(($attributes = RequestAttributesExtractor::extractAttributes($request))['respond'] ?? $request->attributes->getBoolean('_api_respond', false))) {
59+
if (
60+
$controllerResult instanceof Response
61+
|| !(($attributes = RequestAttributesExtractor::extractAttributes($request))['respond'] ?? $request->attributes->getBoolean('_api_respond', false))
62+
|| $attributes && $this->isOperationAttributeDisabled($attributes, self::OPERATION_ATTRIBUTE_KEY)
63+
) {
5364
return;
5465
}
5566

src/EventListener/WriteListener.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
use ApiPlatform\Core\Api\IriConverterInterface;
1717
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
1818
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
19+
use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
1920
use ApiPlatform\Core\Util\RequestAttributesExtractor;
21+
use Symfony\Component\HttpFoundation\Response;
2022
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
2123

2224
/**
@@ -27,6 +29,10 @@
2729
*/
2830
final class WriteListener
2931
{
32+
use ToggleableOperationAttributeTrait;
33+
34+
public const OPERATION_ATTRIBUTE_KEY = 'write';
35+
3036
private $dataPersister;
3137
private $iriConverter;
3238
private $resourceMetadataFactory;
@@ -43,12 +49,19 @@ public function __construct(DataPersisterInterface $dataPersister, IriConverterI
4349
*/
4450
public function onKernelView(GetResponseForControllerResultEvent $event): void
4551
{
52+
$controllerResult = $event->getControllerResult();
4653
$request = $event->getRequest();
47-
if ($request->isMethodSafe(false) || !($attributes = RequestAttributesExtractor::extractAttributes($request)) || !$attributes['persist']) {
54+
55+
if (
56+
$controllerResult instanceof Response
57+
|| $request->isMethodSafe(false)
58+
|| !($attributes = RequestAttributesExtractor::extractAttributes($request))
59+
|| !$attributes['persist']
60+
|| $this->isOperationAttributeDisabled($attributes, self::OPERATION_ATTRIBUTE_KEY)
61+
) {
4862
return;
4963
}
5064

51-
$controllerResult = $event->getControllerResult();
5265
if (!$this->dataPersister->supports($controllerResult, $attributes)) {
5366
return;
5467
}

src/Metadata/Resource/Factory/InputOutputResourceMetadataFactory.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,20 @@ private function getTransformedOperations(array $operations, array $resourceAttr
6666
$operation['output'] = isset($operation['output']) ? $this->transformInputOutput($operation['output']) : $resourceAttributes['output'];
6767

6868
if (
69-
!isset($operation['status'])
70-
&& isset($operation['output'])
69+
isset($operation['input'])
70+
&& \array_key_exists('class', $operation['input'])
71+
&& null === $operation['input']['class']
72+
) {
73+
$operation['deserialize'] ?? $operation['deserialize'] = false;
74+
$operation['validate'] ?? $operation['validate'] = false;
75+
}
76+
77+
if (
78+
isset($operation['output'])
7179
&& \array_key_exists('class', $operation['output'])
7280
&& null === $operation['output']['class']
7381
) {
74-
$operation['status'] = 204;
82+
$operation['status'] ?? $operation['status'] = 204;
7583
}
7684
}
7785

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Metadata\Resource;
15+
16+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
17+
18+
/**
19+
* @internal
20+
*/
21+
trait ToggleableOperationAttributeTrait
22+
{
23+
/**
24+
* @var ResourceMetadataFactoryInterface|null
25+
*/
26+
private $resourceMetadataFactory;
27+
28+
private function isOperationAttributeDisabled(array $attributes, string $attribute, bool $default = false, bool $resourceFallback = true): bool
29+
{
30+
if (null === $this->resourceMetadataFactory) {
31+
return $default;
32+
}
33+
34+
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
35+
36+
return !((bool) $resourceMetadata->getOperationAttribute($attributes, $attribute, !$default, $resourceFallback));
37+
}
38+
}

0 commit comments

Comments
 (0)