Hello,

On a fresh Drupal 8.6.13 standard install with basic_auth and jsonapi enabled.

Between jsonapi 8.x-2.0 and 8.x-2.1 with the same request, There error message is no more in the response body.

For example:

POST /jsonapi/node/page HTTP/1.1
Host: default.web.entity-share.docker.localhost
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
Authorization: Basic YWRtaW46YWRtaW4=
cache-control: no-cache
Postman-Token: a76e2355-77f5-4f53-a108-2d890ccd12ae
{
  "data": {
    "type": "node--page",
    "id": "11111111-1111-1111-1111-111111111111",
    "attributes": {
      "title": ""
    }
  }
}

There is a 422 error because there is no title.

On 8.x-2.0, I obtain the body:

{
    "errors": [
        {
            "title": "Unprocessable Entity",
            "status": 422,
            "detail": "title: This value should not be null.",
            "source": {
                "file": "/project/www/modules/contrib/jsonapi/src/Controller/EntityResource.php",
                "line": 188,
                "pointer": "/data/attributes/title"
            },
            "meta": {
                "exception": "Drupal\\jsonapi\\Exception\\UnprocessableHttpEntityException: Unprocessable Entity: validation failed. in /project/www/modules/contrib/jsonapi/src/Controller/EntityResource.php:188\nStack trace:\n#0 /project/www/module
...

And nothing on 8.x-2.1.

I can't see anything in the release notes and I am also confused because I see automated tests on the 422 error code and testing the error message.

Is there a new requirement/header I missed between those two versions?

Also there is no error message on the last dev version.

I have been able to isolate the commit. It is from a45056a Issue #3022584 by gabesullice, Wim Leers, e0ipso: Consolidate and simplify NormalizerValue objects: introduce CacheableNormalization. On this commit there is no more error message.

Thanks for any help.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Grimreaper created an issue. See original summary.

Wim Leers’s picture

Status: Active » Postponed (maintainer needs more info)
Issue tags: +API-First Initiative, +Needs tests

That definitely sounds like a regression :(

Can you provide concrete steps to reproduce this? Is the first sample request sufficient to reproduce it with for example article nodes?

Grimreaper’s picture

Status: Postponed (maintainer needs more info) » Active

Thanks Wim for your quick reply!

Yes, the sample request is sufficient.

Steps:

  1. Fresh Drupal 8.6.13 install
  2. Enable jsonapi and basic_auth (or another authentication module)
  3. Do the following request (POSTMAN export):
POST /jsonapi/node/page HTTP/1.1
Host: default.web.entity-share.docker.localhost
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
Authorization: Basic YWRtaW46YWRtaW4=
cache-control: no-cache
Postman-Token: a76e2355-77f5-4f53-a108-2d890ccd12ae
{
  "data": {
    "type": "node--page",
    "id": "11111111-1111-1111-1111-111111111111",
    "attributes": {
      "title": ""
    }
  }
}

The provided "id" is optional.

Wim Leers’s picture

Thanks for confirming! The 2.0 behavior is to get a 422 response, what is the behavior in >=2.1? 422 but just without the useful error message?

Probably won't have time soon to work on this. If you could write a regression test, that'd be helpful.

Grimreaper’s picture

Yes, same error code, but no more error message.

Sorry, also not the time to write a regression test.

Also as said in the issue summary, I am skeptical because I see in the automated tests that the error code 422 with the expected message is tested.

Wim Leers’s picture

Title: No more error message » No more "This value should not be null" error message when sending an empty node title
Status: Active » Needs review
Issue tags: -Needs tests
FileSize
1.2 KB

Since you've done so much for this module, I've put aside my other work for a moment and wrote the test for you 🙂 ❤️

This is passing tests just fine for me, at least for Node. So … I think something else is going on on your site? Or perhaps I misunderstood and this test is not testing what you're saying?

Status: Needs review » Needs work

The last submitted patch, 6: 3042124-6.patch, failed testing. View results

Wim Leers’s picture

Status: Needs work » Postponed (maintainer needs more info)

#6 forgot to ensure required fields are present, hence the failures on some entity types. But it does pass for Node. Which shows is why I think something else is going on on your site.

Hopefully this helps you narrow it down!

Grimreaper’s picture

Thanks a lot Wim!

I don't have the feeling to have done a lot for this module, especially since I don't have time to go further on entity share, but thanks!

The test is exactly what needs to be tested. I have run the automated test and the patch and I agree it works on node.

I don't think it is due my environment as the example is not done on my project but on a sandbox environment I use to isolate contrib / project related bugs. And I have remade the install on this sandbox environment.

I don't have more time today but I don't know, maybe I can make a small video/gif to show the problem.

Wim Leers’s picture

Are you testing with node--page as the sample request body is showing, or is it actually happening on a custom entity type?

Grimreaper’s picture

Yep, standard page content type. No change after standard profile install is done.

I don't have changed the request, it is the real one I have tested with. No anonymisation :).

I tried to make a simplytest.me but it seems the service is not as stable as before.

Wim Leers’s picture

Woah, this is getting very bizarre. You say you can consistently reproduce the problem. But you say that my patch is also passing tests for you.

I don't understand yet how both of those things can happen at the same time. :O

gabesullice’s picture

Category: Support request » Bug report
Status: Postponed (maintainer needs more info) » Active

While working on something unrelated to this, I think I encountered the same error. I also realized what's likely causing the discrepancy between the test and manual replication...

@Grimreaper is including authorization for this request and the authorization he is using is for an admin user. This user likely has the access site reports permission; the test user does not.

I was able to replicate this bug by using one user with this permission and one without it.

The failure I encountered is a failure to encode JSON because of a detected recursion. That happens in src/EventSubscriber/ResourceResponseSubscriber.php:

$response->setContent($serializer->encode($jsonapi_doc_object->getNormalization(), $format));

The source of the recursion appears to be in src/Normalizer/HttpExceptionNormalizer.php:

if ($this->currentUser->hasPermission('access site reports')) {
  // The following information may contain sensitive information. Only show
  // it to authorized users.
  $error['source'] = [
    'file' => $exception->getFile(),
    'line' => $exception->getLine(),
  ];
  $error['meta'] = [
    'exception' => (string) $exception,
    'trace' => $exception->getTrace(),
  ];
}

Specifically, the trace array key. Commenting this out, I am able to receive a response body, with it, I am not.

I have not found out the root cause of this regression. I'm not terribly surprised by it, we did make changes to normalization and exceptions in #3022584: Consolidate and simplify NormalizerValue objects: introduce CacheableNormalization, which @Grimreaper identified as the commit which introduced the bug.

Grimreaper’s picture

@gabesullice Thanks for finding the bug!

At least, I am not crazy :).

Ok, I didn't have suspected a permission.

Wim Leers’s picture

EDIT: nope, sorry, this is not at all related. Please ignore, sorry for the noise!

Ran into this too at #3035134-9: Compatibility with upcoming JSON:API 2.4 release: JSON:API Extras 3.5.

After more hours of debugging than I'd like, I discovered the root cause for #3035134: #3038308: Avoid translations DELETE data loss and unintended changes with PATCH and test all methods against entity route parameter translation upcasting removed the basic_auth module as one of the installed-by-default modules. JSON:API Extras was using Basic Auth for authentication. So this is just another occurrence of #3027890: When trying to use `Authorization: Basic …` header and basic_auth module is not installed, warn user.

Is that perhaps also what's going on here, @Grimreaper? I can tell you're using Basic Auth request headers, but perhaps you don't have the Basic Auth module installed?

I suspect that #13 is actually correct, and there's another way to get to a "403 without explanation" case.

Grimreaper’s picture

No problem.

Thanks for keeping pushing JSONAPI.

gabesullice’s picture

Priority: Normal » Major

Bumping this up to major because I think it's quite common to use the admin role when developing/debugging and this bug makes it nearly impossible to do so. It hides all error messages, whether they're validation errors or access reasons. Worse, HTTP status codes are unaffected thus making it unclear that this is not the intended behavior of the module. That's not good for people that will be trying out JSON:API for the first time now that it's just been added to core.

If we need to fix this quickly and simply, I'd be +1 to commenting out the trace key, adding a todo and filing a lower priority follow up.

gabesullice’s picture

Also, if you are in production and an admin, validation errors disappear which could break the functionality of some sites.

gabesullice’s picture

Title: No more "This value should not be null" error message when sending an empty node title » Empty response body when user has 'access site reports' permission and an exception is thrown; trace cannot be encoded because of recursion detection.
Version: 8.x-2.1 » 8.x-2.x-dev
Component: Miscellaneous » Code

Edit: duplicate comment

logickal’s picture

Status: Active » Needs review
FileSize
721 bytes

Attaching quick patch based on @gabesullice's comments in #17.

Status: Needs review » Needs work

The last submitted patch, 20: jsonapi-trace_recursion_issue-3042124-20.patch, failed testing. View results

logickal’s picture

Status: Needs work » Needs review
FileSize
1.78 KB

It helps if you account for the tests as well. Added the same TODO in HttpExceptionNormalizerTest.php. I will also try to trace the actual recursion when I get some cycles between now and Seattle.

Wim Leers’s picture

#13:

This user likely has the

access site reports permission

; the test user does not.

Based on @Grimreaper's report, I wrote the patch in #6. But even if I change that to first $this->grantPermissionsToTestedRole(['access site reports']);, I still can't reproduce it.

Specifically, the trace array key. Commenting this out, I am able to receive a response body, with it, I am not.

So … there must be some exception that does not offer a trace then, since only commenting out that line means only disabling the $exception->getTrace() call. But: \Exception::getTrace() is defined on PHP's built-in \Exception, so all exceptions must have it. And we know that it's definitely an exception, because the code is in protected function buildErrorObjects(HttpException $exception) {…}, which clearly has a typehint that restricts it to this subset.

Furthermore, \Drupal\Core\EventSubscriber\FinalExceptionSubscriber::onException() also calls ->getTrace(), without problems.

If we need to fix this quickly and simply, I'd be +1 to commenting out the trace key, adding a todo and filing a lower priority follow up.

I don't think we can do that; we need to fix this both in the contrib module and in the committed-to-core module. And we can't just fix this without a regression test in core — I doubt core committers would be willing to accept it.

I did some more digging, and I wonder if this is the cause?

Wim Leers’s picture

Without concrete steps to reproduce (such as the recursion that @gabesullice reported in #13), it's hard to fix this :(

benjy’s picture

I have the same issue registering a user, use the following payload twice, and the 2nd request will fail with this error, because validation fails due to the email already being in use.

POST /jsonapi/user/user

{
   "data":{
      "type":"user--user",
      "attributes":{
         "mail":"asdf@example.com",
         "pass":{
            "value":"asdf"
         },
         "name":"email_registration_123",
         "status":1
      }
   }
}

It's the \Symfony\Component\HttpFoundation\Request in the trace that causes the issue, if I unset that then the encoding works, not sure how to go about finding how that object causes the issue but a good guess might be a referenced to one of the violations? \Drupal\Core\Entity\EntityConstraintViolationList

Wim Leers’s picture

#25: Ah, awesome, concrete steps to reproduce! 🥳 Do you have the bandwidth to add a regression test with that exact sequence of steps to \Drupal\Tests\jsonapi\Functional\JsonApiRegressionTest?

benjy’s picture

I wrote the test but it passes against Drupal 8 core, further investigation required!

Wim Leers’s picture

Thanks so much for writing that test! It's very unfortunate that the test passes if the manual testing repeatedly causes this problem. Can you still manually reproduce #25?

gabesullice’s picture

Project: JSON:API » Drupal core
Version: 8.x-2.x-dev » 8.7.x-dev
Component: Code » jsonapi.module
FileSize
2.89 KB
2.38 KB

Ugh, it's unfortunate that #27 couldn't replicate the error so that this could land before the 8.7 RC. I shouldn't have speculated about which permission caused the error. The simplest thing we can do is run the request as a regular user and another as an admin user to show the issue. This should demonstrate the error.

Moving this to the core queue.

Update:

This proves the bug:

1) Drupal\Tests\jsonapi\Functional\JsonApiRegressionTest::testRecursionDetectedWhenResponseContainsViolationsFrom3042124
Failed asserting that null is not null.

/var/www/html/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php:952

The assertions pass for the regular user, but fail for the admin user (evidenced by the line number).

Status: Needs review » Needs work

The last submitted patch, 29: 3042124-29.patch, failed testing. View results

gabesullice’s picture

Great! #29 worked (failed) just as expected.

I created a backport of the test (to the contrib 2.x JSON:API) and used it with git bisect to confirm @Grimreaper's isolation of #3022584: Consolidate and simplify NormalizerValue objects: introduce CacheableNormalization as the culprit.

Investigating the root cause now.

gabesullice’s picture

I found the root cause. Prior to #3022584: Consolidate and simplify NormalizerValue objects: introduce CacheableNormalization, HttpExceptionNormalizerValue extended FieldItemNormalizerValue, which had a method called rasterizeValueRecursive. That recursive method would recurse into the trace and normalize all its values. For values it encountered that were non-scalars/non-arrays (like Symfony's Request object, present in the trace as an argument to a function in the call stack), it would attempt to call __toString on them and fall back to NULL otherwise.

After #3022584, we no longer called rasterizeValueRecursive for exceptions. So, when the encoder attempted to encode the "normalized" value. It really wasn't normalized. Full objects still existed in the value to be encoded. The encoder tried to handle this gracefully by serializing the object regardless. However, the request object present in the trace has a reference to the current exception. That exception of course has a trace too, which has the request, which has the exception... ... which was the infinite loop that we encountered in this issue.

The solution is fairly simple. We need to restore the recursive rasterization that we previously had for exceptions and have since lost.

I'm doing that now, but I don't yet have a patch. I'm only recording my thoughts here because it's almost EOD and I may need to pick this up again tomorrow.

gabesullice’s picture

Status: Needs work » Needs review
FileSize
3.8 KB
6.18 KB

And the fix.

I moved the protected rasterizeValueRecursive up into the shared base class between FieldItemNormalizer and HttpExceptionNormalizer and then made sure it was called by HttpExceptionNormalizer::normalize.

The end result doesn't change any internal APIs or change the intended JSON:API output. It only restores the output that should have been there all along (i.e. the missing HTTP response body).

gabesullice’s picture

Title: Empty response body when user has 'access site reports' permission and an exception is thrown; trace cannot be encoded because of recursion detection. » Empty response body when user is an administrator and an exception is thrown; some traces cannot be encoded because of recursion detection.
Wim Leers’s picture

Title: Empty response body when user is an administrator and an exception is thrown; some traces cannot be encoded because of recursion detection. » [regression] Empty response body when user is an administrator and an exception is thrown; some traces cannot be encoded because of recursion detection.
Status: Needs review » Reviewed & tested by the community
Issue tags: +DX (Developer Experience)

What can I say?

GABE IS BACK!!!!!

Not only did he get to the bottom of this, he's also sprinkling our tests with hilarious references 😆👏

  1. +++ b/core/modules/jsonapi/src/Normalizer/FieldItemNormalizer.php
    @@ -132,42 +132,6 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
    -  protected static function rasterizeValueRecursive($value) {
    
    +++ b/core/modules/jsonapi/src/Normalizer/NormalizerBase.php
    @@ -20,6 +21,42 @@
    +  protected static function rasterizeValueRecursive($value) {
    

    So we're moving this into a parent (base) class. That's fine.

  2. +++ b/core/modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php
    @@ -49,7 +49,7 @@ public function __construct(AccountInterface $current_user) {
    -    return new HttpExceptionNormalizerValue(new CacheableMetadata(), $this->buildErrorObjects($object));
    +    return new HttpExceptionNormalizerValue(new CacheableMetadata(), static::rasterizeValueRecursive($this->buildErrorObjects($object)));
    

    This is the fix. We're wrapping an existing function call in one additional function call. That's fine.

  3. +++ b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php
    @@ -905,4 +905,52 @@ public function testMapFieldTypeNormalizationFromIssue3040590() {
    +    $user = $this->drupalCreateUser(['bypass node access'], 'Sam');
    +    $admin = $this->drupalCreateUser([], 'Gandalf', TRUE);
    

    🤩

gabesullice’s picture

#29 is the test only patch.

benjy’s picture

This looks good to me also, nice work.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed 49a7dfcb47 to 8.8.x and 94fcd54332 to 8.7.x. Thanks!

  • alexpott committed 49a7dfc on 8.8.x
    Issue #3042124 by gabesullice, Wim Leers, logickal, benjy, Grimreaper: [...

  • alexpott committed 94fcd54 on 8.7.x
    Issue #3042124 by gabesullice, Wim Leers, logickal, benjy, Grimreaper: [...
Wim Leers’s picture

Ported to the JSON:API contrib module: https://git.drupalcode.org/project/jsonapi/commit/52534b2

Status: Fixed » Closed (fixed)

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