Problem/Motivation

While working on #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method I was under the false impression that it was intentional that HAL+JSON request's error responses always used application/json. But no, that is a bug! It's simply because ExceptionHalJsonSubscriber extends ExceptionJsonSubscriber which returns JsonResponse objects. This was done in #2472323: Move modal / dialog to query parameters without actual consideration, probably because we had close to zero test coverage for REST (and definitely zero test coverage to verify consistent behavior across different formats).

But since #2472323: Move modal / dialog to query parameters, we've introduced \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber. That class automatically handles all serialization formats. json and hal_json are serialization formats. Therefore we can simply remove ExceptionHalJsonSubscriber.

As of #2739617: Make it easier to write on4xx() exception subscribers, we can really "just" remove ExceptionHalJsonSubscriber, because as of that issue, the inherent flaws in \Drupal\Core\EventSubscriber\HttpExceptionSubscriberBase's design have been fixed, which allows \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber to easily handle all 4xx responses for a given format. (Before that, you had to implement a separate method for every 4xx status code, which led to super confusing chaos.)

Proposed resolution

Step 1: remove Drupal\hal\EventSubscriber\ExceptionHalJsonSubscriber.
Done in #10.
This results in exceptions for HAL+JSON requests being handled by \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber. Which means application/hal+json error responses instead of application/json error responses.
This also means we have to update the expectations in the ResourceTestBase subclasses, i.e. all the test coverage introduced by #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method. i.e. making this change, in all HAL+JSON test coverage:
-  protected static $expectedErrorMimeType = 'application/json';
+  protected static $expectedErrorMimeType = 'application/hal+json';
Step 2: handle the edge case of fatal errors.
See #22.
Fatal PHP errors and unhandled exceptions cause \Drupal\Core\EventSubscriber\DefaultExceptionSubscriber to be called and result in application/json responses. We have #2842465: Behavior of fatal PHP errors/uncaught exceptions results is mostly undefined: \Drupal\Core\EventSubscriber\DefaultExceptionSubscriber::onJson() is the de facto catch-all to fix that. Fixing that is out of scope here because it requires changes deep in the request processing system and base system.
Step 3: remove \Drupal\Tests\rest\Functional\ResourceTestBase::$expectedErrorMimeType, since it's no longer necessary
See #25.
We only had \Drupal\Tests\rest\Functional\ResourceTestBase::$expectedErrorMimeType to accommodate sane testing of the HAL+JSON format. Since the HAL+JSON format now behaves sanely, we can make this significant simplification to ResourceTestBase and its subclasses.

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

Wim Leers’s picture

Title: ?_format=hal_json error responses are in text/plain (yet Content-Type header says application/hal+json), ?_format=json error responses are in application/json » ?_format=hal_json error responses are in text/plain (yet Content-Type header says application/hal+json), ?_format=json error responses are in application/json, and mention a PHP fatal
Component: hal.module » serialization.module
Issue summary: View changes
Issue tags: +Needs tests

Oh, wait, I forgot one important bit!

?_format=hal_json
Response:
Content-Type: application/hal+json
No authentication credentials provided.
?_format=json
Response:
Content-Type: application/json
{"message":"A fatal error occurred: No authentication credentials provided."}

The first is wrong because of the reasons I first explained in the issue summary. But the ?_format=json case is also wrong, because it mentions A fatal error occurred:, i.e. somehow this is a PHP fatal error message that ends up in the response, which should definitely not happen.

Since both are wrong, moving to the serialization.module component.

dawehner’s picture

To fix that we probably need a on401 on \Drupal\Core\EventSubscriber\ExceptionJsonSubscriber or you know, #2739617: Make it easier to write on4xx() exception subscribers

damiankloip’s picture

Seems like nothing to do with serialization module itself. The serializer just...serializes stuff :) Anything to do with authentication is out of its remit.

lhangea’s picture

Here are some results for my tests:

curl -i http://localhost:5012/node/1?_format=json
Result:
Content-Type: application/json
{"message":""}

curl -i http://localhost:5012/node/1?_format=hal_json
Result:
Content-Type: text/plain; charset=UTF-8
No authentication credentials provided

curl -i -H "Accept: application/json" http://localhost:5012/node/1?_format=json
Result:
Content-Type: application/json
{"message":""}

curl -i -H "Accept: application/hal+json" http://localhost:5012/node/1?_format=hal_json
Result:
Content-Type: application/json
{"message":"A fatal error occurred: No authentication credentials provided."}

It seems like my results are a bit different of what's described in the IS. Should the request look different ?

For these tests I enabled the needed modules and unchecked the
View published content permission for anonymous users.

Wim Leers’s picture

Wim Leers’s picture

Title: ?_format=hal_json error responses are in text/plain (yet Content-Type header says application/hal+json), ?_format=json error responses are in application/json, and mention a PHP fatal » ?_format=hal_json error responses can be in text/plain or application/json, yet should be application/hal+json

@dawehner had a suspicion in the right direction in #3. The root cause is not the HAL module. It's not even the serialization module. It's the terribleness that is \Drupal\Core\EventSubscriber\HttpExceptionSubscriberBase and the effect it has on classes building on that base class.

  1. The case (401) where a ?_format=hal_json request results in a text/plain response, is one that nor \Drupal\Core\EventSubscriber\ExceptionHalJsonSubscriber nor \Drupal\Core\EventSubscriber\ExceptionJsonSubscriber nor \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber() implement, but that gets handled by \Drupal\Core\EventSubscriber\DefaultExceptionSubscriber
  2. The case where a ?_format=json request results in a application/json response is one that is implemented by \Drupal\Core\EventSubscriber\ExceptionJsonSubscriber (which \Drupal\Core\EventSubscriber\ExceptionHalJsonSubscriber extends and hence inherits)

While working on #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method I was under the false impression that it was intentional that HAL+JSON request's error responses always used application/json. But no, that itself is a bug too! It's simply because ExceptionHalJsonSubscriber extends ExceptionJsonSubscriber which returns JsonResponse objects. This was done in #2472323: Move modal / dialog to query parameters without actual consideration, probably because we had close to zero test coverage for REST (and definitely zero test coverage to verify consistent behavior across different formats).

But since #2472323: Move modal / dialog to query parameters, we've introduced \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber. That class automatically handles all serialization formats. json and hal_json are serialization formats. Therefore we can simply remove ExceptionHalJsonSubscriber.

P.S.: this also made me discover #2833469: Remove 'exception.custom_page_json' service: unused, dead, even with wrong arguments!.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers
Issue tags: +Needs issue summary update
Wim Leers’s picture

Title: ?_format=hal_json error responses can be in text/plain or application/json, yet should be application/hal+json » [PP-1] ?_format=hal_json error responses can be in text/plain or application/json, yet should be application/hal+json
Status: Active » Postponed
Related issues: +#2739617: Make it easier to write on4xx() exception subscribers

It looks like #2739617: Make it easier to write on4xx() exception subscribers is already solving this! Postponing on that issue.

Wim Leers’s picture

FileSize
12.8 KB

As of #2739617-83: Make it easier to write on4xx() exception subscribers, it's clear that it won't solve everything that this issue aims to solve, but it will definitely pave the path; allowing this patch to be significantly smaller.

Here's a patch that's relative to #2739617, it will apply once it's committed.

Also note that this issue makes the need for \Drupal\Tests\rest\Functional\ResourceTestBase::$expectedErrorMimeType go away entirely. So the attached patch can be simplified even further. On top of that, ResourceTestBase::assertResourceErrorResponse() can be removed, just using ResourceTestBase::assertResourceResponse() in all cases will be sufficient. In other words: a good amount of simplification is possible thanks to the bugfix in this issue :)

Wim Leers’s picture

FileSize
14.03 KB

Oops, I forgot two hunks.

Wim Leers’s picture

Wim Leers’s picture

Title: [PP-1] ?_format=hal_json error responses can be in text/plain or application/json, yet should be application/hal+json » ?_format=hal_json error responses can be in text/plain or application/json, yet should be application/hal+json
Status: Postponed » Needs review

#2739617: Make it easier to write on4xx() exception subscribers landed, which means this is now unblocked!

Wim Leers’s picture

FileSize
14.03 KB

Reuploading #11 so it can be tested now.

Status: Needs review » Needs work

The last submitted patch, 15: 2805281-11.patch, failed testing.

effulgentsia’s picture

#2739617: Make it easier to write on4xx() exception subscribers was committed to 8.3 only, whereas this issue is filed against 8.2 and #15 was tested on 8.2.

damiankloip’s picture

Version: 8.2.x-dev » 8.3.x-dev
Status: Needs work » Needs review

I say we give it a test against 8.3.x, we might then need to backport both to 8.2.x if needed. It might have a pretty short 8.2.x shelf life though?

damiankloip’s picture

I think we need to re-upload to re-test...

Wim Leers’s picture

I'll look at this again early next week.

Wim Leers’s picture

I missed one spot!

Wim Leers’s picture

FileSize
17.26 KB
2.61 KB

#21 will fail because there's still one case where the response will have Content-Type: application/json instead of Content-Type: application/hal+json: where there's a fatal PHP error.

This results in a call to \Drupal\Core\EventSubscriber\DefaultExceptionSubscriber::onException(), which determines the format at \Drupal\Core\EventSubscriber\DefaultExceptionSubscriber::getFormat(), which does this:

    // Make an educated guess that any Accept header type that includes "json"
    // can probably handle a generic JSON response for errors. As above, for
    // any format this doesn't catch or that wants custom handling should
    // register its own exception listener.
    foreach ($request->getAcceptableContentTypes() as $mime) {
      if (strpos($mime, 'html') === FALSE && strpos($mime, 'json') !== FALSE) {
        $format = 'json';
      }
    }

And so it sends application/json even though the request Accept header says application/hal+json . (And yes, that means we still use Accept header based negotiation here.

Fixing that is very much out of scope here: HttpExceptionSubscriberBase and its subclasses are only designed to handle \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface exceptions, not anything else. Fatal PHP errors and how that is handled by the rest of the system is another problem, that deserves its own issue, so created that: #2842465: Behavior of fatal PHP errors/uncaught exceptions results is mostly undefined: \Drupal\Core\EventSubscriber\DefaultExceptionSubscriber::onJson() is the de facto catch-all.

Wim Leers’s picture

FileSize
1021 bytes
17.26 KB
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
@@ -301,9 +303,10 @@ public function testPostDxWithoutCriticalBaseFields() {
-    // @todo Uncomment, remove next line in https://www.drupal.org/node/2820364.
-    $this->assertResourceErrorResponse(500, 'A fatal error occurred: Field  is unknown.', $response);
-    //$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nfield_name: This value should not be null.\n", $response);
+    // @todo Uncomment, remove next 3 lines in https://www.drupal.org/node/2820364.
+    $this->assertSame(500, $response->getStatusCode());
+    $this->assertSame(['application/json'], $response->getHeader('Content-Type'));
+    $this->assertSame('{"message":"A fatal error occurred: Field  is unknown."}', (string) $response->getBody());

Oops I accidentally deleted the commented line! Restoring it.

The last submitted patch, 21: 2805281-21.patch, failed testing.

Wim Leers’s picture

FileSize
40.44 KB
36.21 KB

I wrote this in #10:

Also note that this issue makes the need for \Drupal\Tests\rest\Functional\ResourceTestBase::$expectedErrorMimeType go away entirely. So the attached patch can be simplified even further. On top of that, ResourceTestBase::assertResourceErrorResponse() can be removed, just using ResourceTestBase::assertResourceResponse() in all cases will be sufficient. In other words: a good amount of simplification is possible thanks to the bugfix in this issue :)

This reroll handles the emphasized part. If committers would prefer to see this happen in another issue, that'd be fine too.

Wim Leers’s picture

  1. +++ b/core/modules/hal/tests/src/Functional/EntityResource/Block/BlockHalJsonAnonTest.php
    @@ -27,9 +27,4 @@ class BlockHalJsonAnonTest extends BlockResourceTestBase {
    -  protected static $expectedErrorMimeType = 'application/hal+json';
    
    +++ b/core/modules/hal/tests/src/Functional/EntityResource/Block/BlockHalJsonBasicAuthTest.php
    @@ -30,11 +30,6 @@ class BlockHalJsonBasicAuthTest extends BlockResourceTestBase {
    -  protected static $expectedErrorMimeType = 'application/hal+json';
    

    This is done everywhere.

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -43,10 +43,9 @@
    - *    that specifies the exact @code $format @endcode, @code $mimeType @endcode,
    - *    @code $expectedErrorMimeType @endcode and @code $auth @endcode for this
    - *    concrete test. Usually that's all that's necessary: most concrete
    - *    subclasses will be very thin.
    + *    that specifies the exact @code $format @endcode, @code $mimeType @endcode
    + *    and @code $auth @endcode for this concrete test. Usually that's all that's
    + *    necessary: most concrete subclasses will be very thin.
    
    @@ -420,7 +419,7 @@ public function testGet() {
    -    $this->assertNotSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type'));
    +    $this->assertNotSame([static::$mimeType], $response->getHeader('Content-Type'));
    
    +++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
    @@ -47,17 +47,6 @@
       /**
    -   * The expected MIME type in case of 4xx error responses.
    -   *
    -   * (Can be different, when $mimeType for example encodes a particular
    -   * normalization, such as 'application/hal+json': its error response MIME
    -   * type is 'application/json'.)
    -   *
    -   * @var string
    -   */
    -  protected static $expectedErrorMimeType = 'application/json';
    
    @@ -318,7 +303,7 @@ protected function assertResourceResponse($expected_status_code, $expected_body,
    -      $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type'));
    +      $this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
    

    Thanks to this: we simply don't need $expectedErrorMimeType anymore! The only reason we needed it, was because of the bug that's fixed in this patch.

    So, we can simplify all REST tests thanks to this. :)

Wim Leers’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Issue summary completely rewritten. Now it accurately describes what the problem is and how it's being fixed. The scope has changed several times due to other REST, Serialization and request processing system issues being fixed!

Wim Leers’s picture

Title: ?_format=hal_json error responses can be in text/plain or application/json, yet should be application/hal+json » ?_format=hal_json error responses are application/json, yet should be application/hal+json

Adjusting title for correctness too.

Wim Leers’s picture

Component: serialization.module » hal.module
Assigned: Wim Leers » Unassigned
Issue tags: -Needs tests

Test coverage is definitely present.

The key change is now in the hal module, so moving to that component.

This is now ready for final review, and can hopefully be RTBC'd.

damiankloip’s picture

This is looking great, just one nit/question really.

  1. +++ b/core/modules/hal/hal.services.yml
    @@ -26,7 +26,3 @@ services:
    -  exception.default_json:
    

    This was overriding the default exception.default_json service before?!

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -430,7 +429,7 @@ public function testGet() {
    +    $this->assertSame(['application/json'], $response->getHeader('Content-Type'));
    

    Should this be using the static::$mimeType property? I might be missing some additional context from the patch.

dawehner’s picture

I totally agree with @damiankloip, the second point is a bit weird.

#31.1 Is also an interesting observation!

Wim Leers’s picture

#31:

  1. Indeed…
  2. No, that's what I explained at length in #22: this must be hardcoded to application/json because \Drupal\Core\EventSubscriber\DefaultExceptionSubscriber handles this (note that's the DefaultExceptionSubscriber in \Drupal\Core, not in \Drupal\serialization!). We've got #2825347: 'Accept' header still is used for determining response format of error responses, ?_format= has no effect AND serialization module's exception subscriber does not support status code 500 error responses to fix that, as the accompanying code comment says:
        // @todo Update in https://www.drupal.org/node/2825347.
        $response = $this->request('GET', $url, $request_options);
        $this->assert406Response($response);
        $this->assertSame(['application/json'], $response->getHeader('Content-Type'));
    
damiankloip’s picture

Status: Needs review » Reviewed & tested by the community

AH, ok. Yes, I remember now. This is RTBC then IMO. It is basically removing stuff we don't need from test classes now - which is great! This is an important fix now too from my perspective as that service is getting overridden.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 51d3d8a and pushed to 8.3.x. Thanks!

When I reviewed the patch I had the same questions as @damiankloip in #31 - nice to see them answered :)

  • alexpott committed 51d3d8a on 8.3.x
    Issue #2805281 by Wim Leers, damiankloip: ?_format=hal_json error...

Status: Fixed » Closed (fixed)

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