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+jsonerror responses instead ofapplication/jsonerror responses. - This also means we have to update the expectations in the
ResourceTestBasesubclasses, 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\DefaultExceptionSubscriberto be called and result inapplication/jsonresponses. 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::$expectedErrorMimeTypeto accommodate sane testing of the HAL+JSON format. Since the HAL+JSON format now behaves sanely, we can make this significant simplification toResourceTestBaseand 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=jsonThe first is wrong because of the reasons I first explained in the issue summary. But the
?_format=jsoncase 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.modulecomponent.Comment #3
dawehnerTo fix that we probably need a on401 on
\Drupal\Core\EventSubscriber\ExceptionJsonSubscriberor you know, #2739617: Make it easier to write on4xx() exception subscribersComment #4
damiankloip 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 commentedHere are some results for my tests:
curl -i http://localhost:5012/node/1?_format=jsonResult:
Content-Type: application/json
{"message":""}
curl -i http://localhost:5012/node/1?_format=hal_jsonResult:
Content-Type: text/plain; charset=UTF-8
No authentication credentials provided
curl -i -H "Accept: application/json" http://localhost:5012/node/1?_format=jsonResult:
Content-Type: application/json
{"message":""}
curl -i -H "Accept: application/hal+json" http://localhost:5012/node/1?_format=hal_jsonResult:
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 contentpermission 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\HttpExceptionSubscriberBaseand the effect it has on classes building on that base class.?_format=hal_jsonrequest results in atext/plainresponse, is one that nor\Drupal\Core\EventSubscriber\ExceptionHalJsonSubscribernor\Drupal\Core\EventSubscriber\ExceptionJsonSubscribernor\Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber()implement, but that gets handled by\Drupal\Core\EventSubscriber\DefaultExceptionSubscriber?_format=jsonrequest results in aapplication/jsonresponse is one that is implemented by\Drupal\Core\EventSubscriber\ExceptionJsonSubscriber(which\Drupal\Core\EventSubscriber\ExceptionHalJsonSubscriberextends 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 becauseExceptionHalJsonSubscriberextendsExceptionJsonSubscriberwhich returnsJsonResponseobjects. 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.jsonandhal_jsonare 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::$expectedErrorMimeTypego 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 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 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 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/jsoninstead 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/jsoneven though the requestAcceptheader saysapplication/hal+json. (And yes, that means we still useAcceptheader based negotiation here.Fixing that is very much out of scope here:
HttpExceptionSubscriberBaseand its subclasses are only designed to handle\Symfony\Component\HttpKernel\Exception\HttpExceptionInterfaceexceptions, 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
$expectedErrorMimeTypeanymore! 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
halmodule, 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 commentedThis is looking great, just one nit/question really.
This was overriding the default
exception.default_jsonservice before?!Should this be using the
static::$mimeTypeproperty? 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/jsonbecause\Drupal\Core\EventSubscriber\DefaultExceptionSubscriberhandles this (note that's theDefaultExceptionSubscriberin\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 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: