Problem/Motivation

When an error or relationship document is serialized, the standard jsonapimember is not added. This is inconsistent with the serialization of individual, collection and related documents.

Since the jsonapi member can be used to improve client-side tooling and helps communicate support features, we should ensure this member is serialized into every response document.

Proposed resolution

Add the standard jsonapi member to every response document.

Remaining tasks

Test cases.
Untangle HttpExceptionNormalizerValue from FieldNormalizerValue.

Comments

gabesullice created an issue. See original summary.

gabesullice’s picture

StatusFileSize
new5.75 KB

Test only patch.

gabesullice’s picture

Title: Error responses are missing the `jsonapi` and `links` top-level members » Error responses are missing the `jsonapi` top-level member
Issue summary: View changes
Related issues: +#2934362: Specify a "code" for every exception that JSON API throws

I removed link.self from the issue title and summary. I no longer think a self link should be in the error response, because a link implies support for the linked resource.

Until we are able to do some logic to determine that we _do_ support the link, we shouldn't be including it. I.e. if we could determine that the error was temporary (like a DB lock), then it should be included, but a link for a bundle which doesn't exist should not be included. We don't have the infrastructure to do that right now and it may not even be possible. Perhaps when the related issue is done, we could reconsider.

wim leers’s picture

Status: Active » Needs work

I think #2 is ready for review?

Ah, no, it's blocked on Untangle HttpExceptionNormalizerValue from FieldNormalizerValue. probably. But that means #2 should be tested to prove that it fails (although it obviously will, so little point).

Changing to Needs work then.

gabesullice’s picture

@Wim Leers, #2 is a test only patch ;)

wim leers’s picture

Yes, I know. By "being ready for review", I meant it probably should be tested by testbot to prove that it's failing. I explained that it'll obviously fail, so little point in doing so.

gabesullice’s picture

Title: Error responses are missing the `jsonapi` top-level member » Error and relationships responses are missing the `jsonapi` top-level member
Issue summary: View changes

I've discovered more places where this is not added to the document. Updated the IS to reflect it.

gabesullice’s picture

Title: Error and relationships responses are missing the `jsonapi` top-level member » Error and relationship responses are missing the `jsonapi` top-level member
wim leers’s picture

Issue tags: +Needs tests
wim leers’s picture

Title: Error and relationship responses are missing the `jsonapi` top-level member » Spec Compliance: Error and relationship responses are missing the `jsonapi` top-level member

The spec says in principle that the top-level jsonapi member is a MAY … but in practice I think it's a MUST in our implementation, because right now it's behaving inconsistently. This violates the spirit of the spec IMHO.

wim leers’s picture

Version: 8.x-1.x-dev » 8.x-2.x-dev
wim leers’s picture

Title: Spec Compliance: Error and relationship responses are missing the `jsonapi` top-level member » Spec Compliance: Error responses are missing the `jsonapi` top-level member
Related issues: +#2948666: Remove JSON API's use of $context['cacheable_metadata']

The scope of this issue just got reduced, because thanks to #2948666: Remove JSON API's use of $context['cacheable_metadata'], relationship responses now do have the jsonapi top-level member!

gabesullice’s picture

Priority: Normal » Minor

Dialing down.

wim leers’s picture

👍

gabesullice’s picture

StatusFileSize
new4.89 KB

Rerolled.

chanduvkp’s picture

Hi,

I am a new for this module. I have patched #15. I appreciate if you give me steps for testing?

Thanks,
Chandu

gabesullice’s picture

Hi @chanduvkp! I'm glad to see that you're interested in this issue!

#15 only provides tests, it does not actually change any production behavior. I use a phpunit command to run tests, but that can vary per site. What are you trying to do?

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new22.16 KB
new26.88 KB

Thanks to the work I've been doing yesterday on #2984911: Remove access to the Request object in the normalization process, I had a better sense of how to approach this issue. Attached patch makes tests pass for Node. It's possible (likely) that more test assertions need to be updated.

FYI: the approach could be even simpler if we didn't have the concept of meta.errors: then JsonApiDocumentTopLevel could continue to receive a single set of values, and if they're all exceptions, then it'd result in a JSON API document with only the errors top-level member.

P.S.: I clearly ran into cacheability-related aspects here, in part helped by #2948666: Remove JSON API's use of $context['cacheable_metadata'], in part a next step to #2948666. This will certainly also help #2929428: [>=8.5] Convert "throw new *HttpException" into "throw new Cacheable*HttpException" where possible!

wim leers’s picture

Issue tags: -Needs tests

Tests were already taken care of by @gabesullice in #15!

Status: Needs review » Needs work

The last submitted patch, 18: 2949807-17.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Assigned: Unassigned » wim leers

Got one detail wrong 😡

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.08 KB
new27.04 KB

The one detail was that the X-Drupal-Cache-Contexts header is being generated for error responses, but there's an empty set of cache contexts X-Drupal-Cache-Contexts: (which parses to ['']). That is what is causing most of the failures in #18, because our test coverage is creating a "merged" response, and in doing so is trying to create a merged response with $cache_contexts = [''], which of course is not a valid cache context, and hence the assertion fails…

This fixes that.

Note that this problem will cease to exist with #2929428: [>=8.5] Convert "throw new *HttpException" into "throw new Cacheable*HttpException" where possible, because then exceptions will have associated cache contexts, and hence error responses will also have associated cache contexts.

Status: Needs review » Needs work

The last submitted patch, 22: 2949807-22.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new2.37 KB
new29.34 KB

Fix some one-off failures.

Status: Needs review » Needs work

The last submitted patch, 24: 2949807-24.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

+++ b/src/JsonApiResource/JsonApiDocumentTopLevel.php
@@ -24,12 +27,20 @@ class JsonApiDocumentTopLevel {
-   * @param \Drupal\Core\Entity\EntityInterface|\Drupal\jsonapi\EntityCollection|\Drupal\jsonapi\LabelOnlyEntity $data
+   * @param \Drupal\Core\Entity\EntityInterface|\Drupal\jsonapi\EntityCollection|\Drupal\jsonapi\LabelOnlyEntity|null $data
...
+  public function __construct($data, $errors = []) {
...
   /**

+++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
+++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
@@ -24,6 +24,16 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface,

@@ -24,6 +24,16 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface,
+   * Whether this is an errors document or not.
...
+  protected $isErrorsDocument;

What if we made an ErrorCollection object that extends \ArrayIterator (i.e. nothing fancy).

With that one could make the case that the single argument that the function receives is its "primary data". That frees us from awkwardly tracking this boolean value and needing these mutually exclusive arguments.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new929 bytes
new29.94 KB

The 4xx-response cache tag is now appearing on responses, and hence also on merged responses. But an individual response being 4xx doesn't cause the collection response to also be 4xx — the collection will still remain a 200 response! This fixes that.

wim leers’s picture

Hah, #27 does the same as #2991389-4: Test coverage: relationship response cacheabiliity! We're converging on finding the same problems from different starting points! 😆

Status: Needs review » Needs work

The last submitted patch, 27: 2949807-27.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new2.44 KB
new35.87 KB

This should make most tests pass.

The problem is that 4xx responses used to be not cacheable and now they are. Hence testCollection()'s automatically generated "expected responses" (which are generated from responses to each individual resource expected to be in the collection) are now merging 4xx responses' cacheability too, which is resulting in these test failures.

wim leers’s picture

Note: I'm quite hopeful that #2991389: Test coverage: relationship response cacheabiliity will land before this and will make this patch simpler!

Status: Needs review » Needs work

The last submitted patch, 30: 2953346-30.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new964 bytes
new30.95 KB

Update UserTest::testQueryInvolvingRoles()'s expectations.

Status: Needs review » Needs work

The last submitted patch, 33: 2949807-32.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new957 bytes
new31.85 KB

Updated MessageTest::testCollection()'s expectations.

Status: Needs review » Needs work

The last submitted patch, 35: 2949807-35.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new783 bytes
new32.57 KB

Updated RestJsonApiUnsupported's expectations.

Status: Needs review » Needs work

The last submitted patch, 37: 2949807-37.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.66 KB
new32.73 KB

Ah, and here's the last bit! This should be green! 🙏🙏🙏🙏🙏🙏🙏

(Stupidly, I had indented this code many hours ago because it needed to be scrutinized again. It somehow ended up being added to one of the interdiffs … and hence I'd forgotten about it. Gah!)

wim leers’s picture

#26: I hadn't forgotten about you/this comment! I just wanted to finish what I started before going onto nice-to-have things like this :)

With that one could make the case that the single argument that the function receives is its "primary data". That frees us from awkwardly tracking this boolean value and needing these mutually exclusive arguments.

I tried that — I explained this in #18:

FYI: the approach could be even simpler if we didn't have the concept of meta.errors: then JsonApiDocumentTopLevel could continue to receive a single set of values, and if they're all exceptions, then it'd result in a JSON API document with only the errors top-level member.

That attempt failed because it detected an "errors only" document by checking that all values in JsonApiDocumentTopLevel were HttpExceptionInterface instances. But doing that makes it impossible to e.g. have a collection response with empty data and only meta.errors (because all nodes are unpublished for example).

Wouldn't the same happen in your proposal?

wim leers’s picture

Assigned: wim leers » Unassigned
gabesullice’s picture

Wouldn't the same happen in your proposal?

No, I don't think so. The approach discussed in #18 was not able to do $this->data instanceof ErrorCollection because it still used an EntityCollection to carry the errors. That made it impossible to determine if it should be an error document or a data document with meta.errors.

In code, what I'm saying is: $this->data instanceof ErrorCollection === $this->isErrorDocument. IOW, we can use a concrete type to carry the information rather than using two mutually exclusive arguments. Then, exceptions in an EntityCollection object must be meta.errors errors.

wim leers’s picture

StatusFileSize
new13.23 KB
new32.88 KB

It wasn't using an EntityCollection to carry errors, it was just a plain array of HttpExceptionInterface objects.

(That's not what the current docs for JsonApiDocumentTopLevel say, I know, but … PHP is not strictly typed and there's no assert(), so … yeah, plain arrays are already being passed in there in HEAD too!)

I like what you said, so I gave it a try … and it works! :) I do slightly prefer your proposal/this patch because it gives us more strict types, rather than array inspection/manipulation logic. I think that as other parts of the logic become stricter typed/cleaned up, the approach introduced here will become gradually simpler to understand.

So: 👍 from me.

gabesullice’s picture

This looks impeccable :) I just have a couple observations.


  1. +++ b/src/JsonApiResource/JsonApiDocumentTopLevel.php
    @@ -20,49 +17,29 @@ class JsonApiDocumentTopLevel {
    -  public function __construct($data, $errors = []) {
    +  public function __construct($data) {
         $this->data = $data;
    -    assert($data !== NULL || !empty($errors));
    -
    -    assert(Inspector::assertAll(function ($v) {
    -      return $v instanceof HttpExceptionInterface;
    -    }, $errors));
    -    $this->errors = $errors;
       }
    

    😗👌

  2. +++ b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
    @@ -170,7 +172,17 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm
    +      $normalizer_values = array_map(function (HttpExceptionInterface $exception) use ($format, $context, $serializer) {
    ...
    +      return new JsonApiDocumentTopLevelNormalizerValue($normalizer_values, ['is_error_document' => TRUE], [], FALSE);
    

    ['is_error_document' => TRUE] could be an independent variable. But that's a super nit and only needed if you agree.

  3. +++ b/src/Encoder/JsonEncoder.php
    diff --git a/src/EventSubscriber/DefaultExceptionSubscriber.php b/src/EventSubscriber/DefaultExceptionSubscriber.php
    index 05c6f4c..25b57d8 100644
    
    +++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
    @@ -75,48 +85,62 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface,
    +    if (!$this->isErrorsDocument) {
    ...
    +      // Every JSON API document contains absolute URLs.
    +      $this->addCacheContexts(['url.site']);
    

    Isn't this true for all documents, not just resource documents? Perhaps we don't put a links object on error documents?

wim leers’s picture

  1. 🙂
  2. Hah! I had it as an independent variable first. I chose this approach for its explicitness and immutability.
  3. Correct, we don't put links on error documents. Plus, the links inside error objects (http://jsonapi.org/format/#error-objects) are all absolute w3.org URLs! So at least for now, this is correct.
gabesullice’s picture

Status: Needs review » Reviewed & tested by the community

2+3: works for me!

Thanks for this awesome work!

wim leers’s picture

Assigned: Unassigned » wim leers
Status: Reviewed & tested by the community » Needs review
StatusFileSize
new32.2 KB

Yay!

Since your RTBC though, both #2985426: Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden and #2991389: Test coverage: relationship response cacheabiliity have been committed, both of which require changes to be made here.

First, let's rebase. Notable changes:

  1. DefaultExceptionSubscriber now has additional deletions; lines that were only added in #2991389!
  2. JsonApiDocumentTopLevelNormalizerValue still makes the same set of changes, but the existing code now looks a little different

Status: Needs review » Needs work

The last submitted patch, 47: 2949807-47.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new994 bytes
new32.1 KB

I got #47.1 wrong! Fixed.

If this works, then one of my main reservations about this patch actually is already fully fixed, thanks to #2985426: Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden! 😁 🍻

wim leers’s picture

StatusFileSize
new3.1 KB
new32.16 KB

Yay, #49 passed on 8.5/8.6/8.7!

Fixing CS violations.

@gabesullice, please re-confirm RTBC.

gabesullice’s picture

Status: Needs review » Reviewed & tested by the community

  • Wim Leers committed c648da4 on 8.x-2.x
    Issue #2949807 by Wim Leers, gabesullice: Spec Compliance: Error...
wim leers’s picture

Assigned: wim leers » Unassigned
Status: Reviewed & tested by the community » Fixed

🚢

wim leers’s picture

Created change record: https://www.drupal.org/node/2991990.

Status: Fixed » Closed (fixed)

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