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 meansapplication/hal+json
error responses instead ofapplication/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 inapplication/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 toResourceTestBase
and its subclasses.
Remaining tasks
None.
User interface changes
None.
API changes
None.
Data model changes
None.
Comment | File | Size | Author |
---|---|---|---|
#25 | 2805281-25.patch | 40.44 KB | Wim Leers |
Comments
Comment #2
Wim LeersOh, wait, I forgot one important bit!
?_format=hal_json
?_format=json
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 mentionsA 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.Comment #3
dawehnerTo fix that we probably need a on401 on
\Drupal\Core\EventSubscriber\ExceptionJsonSubscriber
or you know, #2739617: Make it easier to write on4xx() exception subscribersComment #4
damiankloip CreditAttribution: damiankloip at Tag1 Consulting commentedSeems like nothing to do with serialization module itself. The serializer just...serializes stuff :) Anything to do with authentication is out of its remit.
Comment #5
lhangea CreditAttribution: lhangea commentedHere 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.Comment #6
Wim LeersThis is a blocker for JSON API, see #2829977-11: SerializableHttpException and Drupal\jsonapi\Error\* are not necessary.
Comment #7
Wim Leers@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.?_format=hal_json
request results in atext/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
?_format=json
request results in aapplication/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 becauseExceptionHalJsonSubscriber
extendsExceptionJsonSubscriber
which returnsJsonResponse
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
andhal_json
are serialization formats. Therefore we can simply removeExceptionHalJsonSubscriber
.P.S.: this also made me discover #2833469: Remove 'exception.custom_page_json' service: unused, dead, even with wrong arguments!.
Comment #8
Wim LeersComment #9
Wim LeersIt looks like #2739617: Make it easier to write on4xx() exception subscribers is already solving this! Postponing on that issue.
Comment #10
Wim LeersAs 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 usingResourceTestBase::assertResourceResponse()
in all cases will be sufficient. In other words: a good amount of simplification is possible thanks to the bugfix in this issue :)Comment #11
Wim LeersOops, I forgot two hunks.
Comment #12
Wim LeersThis also blocks #2813853: RequestHandler has its own error handling rather than leaving it to KernelEvents::EXCEPTION event subscribers, so tagging .
Comment #13
Wim LeersTurns out this is also blocking #2827084-65: EntityNormalizer::denormalize should not throw UnexpectedValueException, but \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException, so we get a 422 response instead of 400. blocker++
Comment #14
Wim Leers#2739617: Make it easier to write on4xx() exception subscribers landed, which means this is now unblocked!
Comment #15
Wim LeersReuploading #11 so it can be tested now.
Comment #17
effulgentsia CreditAttribution: effulgentsia at Acquia commented#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.
Comment #18
damiankloip CreditAttribution: damiankloip at Acquia commentedI 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?
Comment #19
damiankloip CreditAttribution: damiankloip at Acquia commentedI think we need to re-upload to re-test...
Comment #20
Wim LeersI'll look at this again early next week.
Comment #21
Wim LeersI missed one spot!
Comment #22
Wim Leers#21 will fail because there's still one case where the response will have
Content-Type: application/json
instead ofContent-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:And so it sends
application/json
even though the requestAccept
header saysapplication/hal+json
. (And yes, that means we still useAccept
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.Comment #23
Wim LeersOops I accidentally deleted the commented line! Restoring it.
Comment #25
Wim LeersI wrote this in #10:
This reroll handles the emphasized part. If committers would prefer to see this happen in another issue, that'd be fine too.
Comment #26
Wim LeersThis is done everywhere.
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. :)
Comment #27
Wim LeersIssue 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!
Comment #28
Wim LeersAdjusting title for correctness too.
Comment #29
Wim LeersTest 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.
Comment #30
Wim Leers#2813853: RequestHandler has its own error handling rather than leaving it to KernelEvents::EXCEPTION event subscribers and #2827084: EntityNormalizer::denormalize should not throw UnexpectedValueException, but \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException, so we get a 422 response instead of 400 are blocked by this, adding them to the related issues.
Comment #31
damiankloip CreditAttribution: damiankloip at Acquia commentedThis is looking great, just one nit/question really.
This was overriding the default
exception.default_json
service before?!Should this be using the
static::$mimeType
property? I might be missing some additional context from the patch.Comment #32
dawehnerI totally agree with @damiankloip, the second point is a bit weird.
#31.1 Is also an interesting observation!
Comment #33
Wim Leers#31:
application/json
because\Drupal\Core\EventSubscriber\DefaultExceptionSubscriber
handles this (note that's theDefaultExceptionSubscriber
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:Comment #34
damiankloip CreditAttribution: damiankloip at Acquia commentedAH, 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.
Comment #35
alexpottCommitted 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 :)
Comment #37
Wim LeersHURRAY!!!!!!!
This unblocked both: