Problem/Motivation

While working on #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method, I ported the "send invalid UUID and see what happens" test coverage from \Drupal\rest\Tests\CreateTest::assertCreateEntityInvalidSerialized(). See #2737719-85: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method.

This is what is sent:

{…, uuid: '<INVALID-UUID>', … }

This works great! But CreateTest was only testing the json format, not the hal_json format… and I want test coverage to be comprehensive, so I applied it to hal_json too. And upon doing so, I noticed this was sadly completely broken.

You'd get error responses like this:

Drupal\Tests\hal\Functional\EntityResource\Comment\CommentHalJsonAnonTest::testPost
Exception: Warning: Invalid argument supplied for foreach()
Drupal\hal\Normalizer\FieldNormalizer->denormalize()() (Line: 68)


/var/www/html/core/lib/Drupal/Core/Test/HttpClientMiddleware/TestHttpClientMiddleware.php:44
/var/www/html/vendor/guzzlehttp/promises/src/FulfilledPromise.php:39
/var/www/html/vendor/guzzlehttp/promises/src/TaskQueue.php:61
/var/www/html/vendor/guzzlehttp/promises/src/Promise.php:246
/var/www/html/vendor/guzzlehttp/promises/src/Promise.php:223
/var/www/html/vendor/guzzlehttp/promises/src/Promise.php:266
/var/www/html/vendor/guzzlehttp/promises/src/Promise.php:225
/var/www/html/vendor/guzzlehttp/promises/src/Promise.php:62
/var/www/html/vendor/guzzlehttp/guzzle/src/Client.php:129
/var/www/html/core/modules/rest/tests/src/Functional/ResourceTestBase.php:254
/var/www/html/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php:429

That's of course not remotely helpful.

Turns out the reason is that HAL's \Drupal\hal\Normalizer\FieldNormalizer::denormalize() is incapable of handling "abbreviated" field structures like the one mentioned above. It demands this:

{…, uuid: [{value: '<INVALID-UUID>'}], … }

Proposed resolution

Either:

  1. Match \Drupal\hal\Normalizer\FieldNormalizer::denormalize()'s behavior
  2. Better error output.

Remaining tasks

TBD

User interface changes

None.

API changes

TBD, hopefully none.

Data model changes

TBD, hopefully none.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

vdanielpop’s picture

Assigned: Unassigned » vdanielpop
vdanielpop’s picture

Assigned: vdanielpop » Unassigned
dawehner’s picture

I think throwing some better error message would be the preferred solution. We should tell people as early as possible when something of their passed input is wrong.

damiankloip’s picture

Status: Active » Needs review
FileSize
4.85 KB

So we could just check that the value that's about to be passed to the denormalization for the field item is an array or not. It should then be handled correctly. Alternatively, we could check for this scalar value and just handle that instead like:

  /**
   * {@inheritdoc}
   */
  public function denormalize($data, $class, $format = NULL, array $context = array()) {
    if (!isset($context['target_instance'])) {
      throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldNormalizer');
    }

    /** @var FieldItemListInterface $items */
    $items = $context['target_instance'];
    $item_definition = $items->getItemDefinition();
    $item_class = $item_definition->getClass();

    if (!is_array($data)) {
      $context['target_instance'] = $items->appendItem();
      $this->serializer->denormalize($data, $item_class, $format, $context);
    }
    else {
      foreach ($data as $item_data) {
        // Create a new item and pass it as the target for the unserialization of
        // $item_data. All items in field should have removed before this method
        // was called.
        // @see \Drupal\serialization\Normalizer\ContentEntityNormalizer::denormalize().
        $context['target_instance'] = $items->appendItem();
        $this->serializer->denormalize($item_data, $item_class, $format, $context);
      }
    }

    return $items;
  }

Even just casting the value to an array would actually get handled correct too, I think...

I made a change to the hal FieldNormalizer as we have the same problem in both. I then realised we have the same code in both - if the hal FieldNormalizer extends the serialization FieldNormalizer we can share this method.

damiankloip’s picture

The last submitted patch, 5: 2820743-5.patch, failed testing.

damiankloip’s picture

Title: HAL's FieldNormalizer is very unforgiving, impossible to debug error response given » FieldNormalizers are very unforgiving, impossible to debug error response given

Since we did #2827218: Denormalization on field items is never called: add FieldNormalizer + FieldItemNormalizer with denormalize() methods this is also a problem for serialization module too. Hence the direction of the patch. Re-titling accordingly.

damiankloip’s picture

Component: hal.module » serialization.module
dawehner’s picture

Here is just a quick review.

  1. +++ b/core/modules/serialization/src/Normalizer/FieldNormalizer.php
    @@ -30,9 +31,18 @@ public function denormalize($data, $class, $format = NULL, array $context = arra
    +      throw new InvalidArgumentException('The field passed in via $context[\'target_instance\'] must have a parent set.');
    ...
    +      throw new UnexpectedValueException('Field values must use an array structure');
    

    Can we use the field name in the exception message? This makes it a bit easier to debug later one day

  2. +++ b/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php
    @@ -117,4 +117,19 @@ public function testFieldNormalizeDenormalize() {
    +   *
    +   * @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException
    +   * @expectedExceptionMessage Field values must use an array structure
    

    Note: We use $this->setExpectedException now

damiankloip’s picture

FileSize
5.21 KB
1.99 KB

Thanks Daniel! Seems like fair points you have there.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

WFM

  • catch committed cf2b9be on 8.3.x
    Issue #2820743 by damiankloip, dawehner: FieldNormalizers are very...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.3.x, thanks!

  • catch committed cf2b9be on 8.4.x
    Issue #2820743 by damiankloip, dawehner: FieldNormalizers are very...

  • catch committed cf2b9be on 8.4.x
    Issue #2820743 by damiankloip, dawehner: FieldNormalizers are very...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Wim Leers’s picture

kevin.dutra’s picture

Just wanted to share our unfortunate experience with regard to this change.

We're on the trailing end of upgrading from 8.2 to 8.4 and just discovered that our external integrations are broken as they are using JSON with abbreviated field structures. When the integrations started failing, I was able to trace it back to this issue. I'm surprised that a BC breaking change like this didn't end up with at least a change record.