I found it very confusing/unexpected that the format was called api_json and not jsonapi. i.e.:

/**
 * Adds api_json as known format.
 */
class JsonapiServiceProvider implements ServiceModifierInterface {

  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')
        ->getClass(), '\Drupal\Core\StackMiddleware\NegotiationMiddleware', TRUE)
    ) {
      // @see http://www.iana.org/assignments/media-types/application/vnd.api+json
      $container->getDefinition('http_middleware.negotiation')
        ->addMethodCall('registerFormat', [
          'api_json',
          ['application/vnd.api+json'],
        ]);
    }
  }

}
CommentFileSizeAuthor
#90 2831137-followup-90.patch6.82 KBWim Leers
#90 interdiff.txt2.2 KBWim Leers
#87 2831137-followup-87.patch6.48 KBgaurav.kapoor
#84 2831137-followup-84.patch6.86 KBWim Leers
#84 interdiff.txt656 bytesWim Leers
#81 2831137-followup-81.patch6.85 KBWim Leers
#76 2831137--interdiff--73-75.txt709 bytese0ipso
#75 2831137--interdiff--73-75.txt2.04 KBe0ipso
#75 2831137--drop-required-format--75.patch18.13 KBe0ipso
#73 2831137--drop-required-format--71.patch18.13 KBe0ipso
#73 2831137--interdiff--69-71.txt2.04 KBe0ipso
#69 2831137--interdiff--62-69.txt3.95 KBe0ipso
#69 2831137--drop-required-format--69.patch17.98 KBe0ipso
#62 2831137--interdiff--55-62.txt6.54 KBe0ipso
#62 2831137--drop-required-format--62.patch6.54 KBe0ipso
#55 2831137--interdiff--52-55.txt1.92 KBe0ipso
#55 2831137--drop-required-format--55.patch15.37 KBe0ipso
#52 2831137--drop-required-format--52.patch14.5 KBe0ipso
#39 interdiff.txt2.37 KBWim Leers
#39 2831137-alt-38.patch7.4 KBWim Leers
#39 2831137-alt-core-bugfix.patch1.1 KBWim Leers
#30 2831137-alt-30.patch6.26 KBWim Leers
#24 2831137-20.patch4.16 KBdawehner
#20 core-patch.patch1.06 KBdawehner
#20 2831137-20.patch3.98 KBdawehner
#11 2831137--drop-required-format--11.patch7.77 KBe0ipso
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

Wim Leers’s picture

Title: Document reason for calling the format 'api_json' and not 'jsonapi' » Document reason for labeling the format 'api_json' and not 'jsonapi'
skyredwang’s picture

The JSONAPI specification states:

JSON API requires use of the JSON API media type (application/vnd.api+json) for exchanging data.

vnd. indicates "media types associated with publicly available products" (link). Here the publicly available product is api+json.

The character "+" needs to be escaped when used in URL. Therefore, changing it to "_"(underscore) makes it easier to be recognized.

Wim Leers’s picture

I know that. But it's jsonapi.org and the jsonapi module et cetera. So api_json makes less sense to me than jsonapi.

e0ipso’s picture

We are breaking the specification by not negotiating formats using the standard way (Accept header) in Drupal. We should be using application/vnd.api+json. The closest we can do is ?_format=api_json.

I like api_json better because it does not create confusion between the name of the format and the name of the module. So in the code it's clearer when we're taking module or format. Additionally (and irrelevant) it is similar to hal_json.

Wim Leers’s picture

Issue tags: +Documentation

We are breaking the specification by not negotiating formats

I think that we actually can bring back Accept header-based negotiation for the JSON API module. Because it has separate URLs at which nothing else than Content-Type: application/vnd.api+json is being served. So then we can be completely in line with the spec too.

The https://www.drupal.org/project/services module's D8 branch has code to bring back Accept header negotiation.

I like api_json better because it does not create confusion between the name of the format and the name of the module.

Works for me. Then that should just be documented :)

e0ipso’s picture

I think that we actually can bring back Accept header-based negotiation for the JSON API module.

Would it set the expectation that you can do the same with other formats in core?

Wim Leers’s picture

No, we could make it work just for JSON API routes.

Besides, JSON API routes only support the JSON API format. So I don't see why it's even necessary to specify the format? Why can't we make JSON API routes always return JSON API?

e0ipso’s picture

Besides, JSON API routes only support the JSON API format. So I don't see why it's even necessary to specify the format? Why can't we make JSON API routes always return JSON API?

The only reason I can think of is so the format is detected globally as part of the request. However we could manually set the format by inspecting the route to see if it's JSON API. Maybe we can even drop the api_json altogether.

Thoughts?

hampercm’s picture

+1 for dropping the need to specify _format=api_json on the JSON API module's routes

e0ipso’s picture

The following patch adds support for the Accept header, and is BC. That means that you can use the _format drupalism or the Accept: spec compliant for the format negotiation.

e0ipso’s picture

Status: Active » Needs review
e0ipso’s picture

w00t! Tests passing on the first try. That's new.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers

Reviewing!

hampercm’s picture

One concern I have regarding this approach is that some HTTP proxies fail to correctly pass on the Accept header. The _format Drupalism was created as a work-around for that issue. Assuming there are still buggy proxies out there, the patch in #11 is essentially repeating the same problem that was run into in Core.

Having the JSON API module's routes hard-coded to always use the api_json format seems to me like the safer approach. Otherwise, devs may be stymied by things mysteriously not working, and will wind up needing to use the _format Drupalism to guarantee things work, anyway.

e0ipso’s picture

One concern I have regarding this approach is that some HTTP proxies fail to correctly pass on the Accept header

That contravenes the JSON-API specification in http://jsonapi.org/format/#content-negotiation. I'm up for breaking the spec for a good reason, but breaking the spec because a user may want to use a buggy reverse proxy seems like excessive babysitting for me.

The rationale behind the current implementation is:

  1. Use the Accept header and profit.
  2. If you're using one of those edge cases, then we don't let you stranded and offer the _format alternative.

Thoughts?

Wim Leers’s picture

Component: Documentation » Code
Assigned: Wim Leers » Unassigned
Status: Needs review » Needs work

I'm up for breaking the spec for a good reason, but breaking the spec because a user may want to use a buggy reverse proxy seems like excessive babysitting for me.

This is incorrect.

It even can happen when using a proxy, which the user may not have any choice in at all! e.g. your company or university requires all port 80 (i.e. HTTP) traffic to go through a proxy. So when your site supports JSON API, and allows anonymous users to make requests, and happens to be accessed via one of those proxies… things break horribly.


  1. +++ b/src/StackMiddleware/FormatSetter.php
    @@ -0,0 +1,45 @@
    +  public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
    +    // Check if the accept header is set to the official header.
    +    $accept = $request->headers->get('Accept');
    +    if (strpos($accept, 'application/vnd.api+json') !== FALSE) {
    +      // Manually set the format.
    +      $request->setRequestFormat('api_json');
    +    }
    +
    +    return $this->httpKernel->handle($request, $type, $catch);
    +  }
    

    Right now, #15 is right, because this applies to all routes, unlike what the FormatSetter class' docblock claims. If you would limit this to just JSON API routes, this would be fine.

  2. +++ b/src/RequestCacheabilityDependency.php
    @@ -16,9 +16,11 @@ class RequestCacheabilityDependency implements CacheableDependencyInterface {
    +    array_push($contexts, 'headers:Accept');
    
    +++ b/tests/src/Unit/RequestCacheabilityDependencyTest.php
    @@ -42,6 +42,7 @@ class RequestCacheabilityDependencyTest extends UnitTestCase {
    +      'headers:Accept',
    

    Using this cache context is a big problem: the Accept header may contain any value.

    Use the request_format cache context instead.

  3. +++ b/src/StackMiddleware/FormatSetter.php
    @@ -0,0 +1,45 @@
    +    if (strpos($accept, 'application/vnd.api+json') !== FALSE) {
    

    !== FALSE should be === 0, otherwise even Accept: text/html, application/vnd.api+json would match.

e0ipso’s picture

Status: Needs work » Postponed

This may not be doable, since we want to be able set the format very early, so when errors that happen early are triggered the exceptions are normalized in the JSON API format. However, so early in the bootstrap process we cannot know if the current request belongs or not to a JSON API route.

This is the conversation that happened yesterday in IRC:

<e0ipso> e0ipso I was thinking about
<e0ipso> https://www.drupal.org/node/2831137
<e0ipso> And it seems taht the consensus is
<e0ipso> that we ignore the Accept header
<e0ipso> and just set the format
<e0ipso> for the JSON API routest
<e0ipso> hampercm do you feel this is true?
<e0ipso> WimLeers ^
<hampercm> e0ipso: I’d love to be fully spec compliant and use the Accept header, but I just think that’s going to cause problems. I’d prefer ignoring the header. It’s a spec violation, but it seems a minor one to me
<e0ipso> cool
<e0ipso> so we are all on the same page
<e0ipso> Then the issue is, that we want to set the format very early in the bootstrap (we don't know what could rely on the correct format being set)
<e0ipso> When the http_middleware.negotiation service does its magic, it's so early that we don't even know which route the current request belongs to
<e0ipso> and doing regular expressions on the URL does not seem ideal because we need a lot of context (base url, language negotiation, …)
<e0ipso> So the big problem is, how do we detect that the incoming request is for JSON API to add the format?
<neclimdul> sadly vary support is wonky so drupal doesn't support Accept only negotiation
<hampercm> and it’s more the client violating the spec than the server. The serve is just being forgiving
<e0ipso> neclimdul, hampercm how would you detect that the request is JSON API without having access to anything else but the Request object?
<neclimdul> e0ipso: looking at patch for context but you're expected to use $request->getRequestFormat() right?
<neclimdul> that way the request stack and negotiation and modify a machine friendly value
<***> Buffer Playback...
<neclimdul> [22:11:37] e0ipso: maybe i'm missing something but why isn't this just using the normal negotiation, its quite a bit cheaper then adding a new middleware to every request since its compiled into the container
<hampercm> [22:12:07] e0ipso: I was thinking the module would simply assume all requests sent to its routes were JSON API, and let the deserialization and normalization code figure out if it’s a valid request.
<neclimdul> [22:13:05] additionally, with the format registered, other code outside the module could use the format in standard/expected symfony/d8 ways
<hampercm> [22:14:55] neclimdul: the format is already registered using normal Drupal means by the module, we’re exploring the possibility of answering responses without requiring the client to use the non-standard ‘_format’ query parameter
<hampercm> [22:15:32] answering *requests
<neclimdul> [22:15:38] oh, then no. dont' do it
<neclimdul> [22:18:15] 1) you can't just change the format. You have to provide a Vary header if the url can return more then one format
<neclimdul> [22:19:03] 2) even proxies that sort of work with Vary: Accept can have confusing rules around how it works and its likely to fail
<neclimdul> [22:21:19] source: I spent weeks storming around my house cursing at the state of the internet trying to work out how core could support negotiation without _format as part of he grant to add Accept header support into core.
<***> Playback Complete.
<e0ipso> neclimdul in this particular case, those routes *only* support one single format
<e0ipso> it's not the same for /node/1234
<e0ipso> > simply assume all requests sent to its routes were JSON API
<e0ipso> hampercm we need to set the format to serialize error messages
<e0ipso> hampercm that means that we need to set it early, before any error can slip through the cracks
<neclimdul> e0ipso: what happens when they don't have the correct accept format?
<e0ipso> The routing system refuses the request
<e0ipso> cause the format is a requirement
<e0ipso> neclimdul I have not tried dropping that requirement
<neclimdul> well the routing system surely would
<neclimdul> that's deep in symfony's matching if i remember correctly.
<dawehner_> e0ipso: so you want to set the format before the routing system itself?
<e0ipso> dawehner_ ideally, yes
<e0ipso> just like the http_middleware.negotiation does with the qs parameter
<dawehner_> e0ipso: something like a event subscriber with prio > 32 would work IMGO
<e0ipso> but I don't think I have enough info
<dawehner_> e0ipso: a middleware would be even before that, good idea
<e0ipso> dawehner_ I may need to do event subscriber with prio = 31
<e0ipso> :sad trombone:
<dawehner_> e0ipso: well prio 31 wouldn't work
<e0ipso> I'll report back …
<dawehner_> e0ipso: it runs after routing, so the access checking will be executed already
<dawehner_> so 403 wouldn't get it
<e0ipso> dawehner_ yeah. The alternative is to duplicate the route matching for the request to find out if the request is jsonapi or not
<e0ipso> but that seems less than ideal
<e0ipso> for performance reasons
<dawehner_> e0ipso: hehe, to say the least
<dawehner_> e0ipso: its tricky, as you would also need to do language prefixes or so
<e0ipso> dawehner_ yes. That's the biggest problem
<e0ipso> and having access to the base url, multisites, …
<dawehner_> e0ipso: mh, so which error messages do we talk about?
<dawehner_> e0ipso: for example 403s should work already with the right format?
<e0ipso> dawehner_ context: we want to get rid of the ?_format=api_json
<e0ipso> because it's redundant
<e0ipso> since you're requesting a URL that only supports the jsonapi format
<e0ipso> so, we want to detect that the current request
<e0ipso> is for a JSON API route
<e0ipso> to set the format as early as possible
<e0ipso> we need to set the format early, because we need to serialize exceptions to the JSON API format
<e0ipso> dawehner_ the issue is: that if we hook early enough to catch all possible errors we don't know which route the request is (so we don't know it's for JSON API)
<dawehner_> e0ipso: so you wont to serialize every routing level as jsonapi I guess?
<e0ipso> dawehner_ if we act too late (as in prio 31) we have enough info, but we may miss some errors that will be displayed in HTML
<dawehner_> e0ipso: so 403, 422 at so on and so forth?
<e0ipso> exactly
<e0ipso> I don't think we can solve this with a satisfactory solution
<dawehner_> e0ipso: mhhh
<dawehner_> e0ipso: I think you can solve it pretty easy
<dawehner_> e0ipso: I mean why can't you set _format onto the route defaults?
<dawehner_> e0ipso: mhh, this doesn't take into account router filters, I guess?
<e0ipso> dawehner_ you cannot set it to the route because you also need to act befor the route has been matched for the current request
<e0ipso> need to act == need to have the format already set to api_json
e0ipso’s picture

Title: Document reason for labeling the format 'api_json' and not 'jsonapi' » Assume the use of the 'api_json' format for routes managed by JSON API
dawehner’s picture

So I played around a bit with it, and found a "good" spot to change stuff before the router Request format route filter, ... another route filter.
In order to place one before that we need a core patch which allows priorities to be set for those.

Note: With that we have a passing test suite for jsonapi, but I'm not sure its really right

e0ipso’s picture

Status: Postponed » Needs review

Making tests fail.

The last submitted patch, 20: 2831137-20.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 20: core-patch.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
4.16 KB

This time with the actual files ...

Status: Needs review » Needs work

The last submitted patch, 24: 2831137-20.patch, failed testing.

e0ipso’s picture

dawehner’s picture

e0ipso’s picture

Title: Assume the use of the 'api_json' format for routes managed by JSON API » [PP-1] Assume the use of the 'api_json' format for routes managed by JSON API

Adding this later when the core issue is resolved should not break BC, since the additional ?_format=api_json in the scenario where it's not needed will not make any difference. Agree?

dawehner’s picture

Can't agree more ...

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
6.26 KB

#28+#29: agreed!

But I honestly wonder why it can't be done in a much simpler way.

We already have

->setRequirement('_format', 'api_json')

This means the api_json format is the only one supported, and that users must specify this format in their Accept header or ?_format query string.

But if this is the only format that is supported, why don't we just omit that requirement, just like we do for HTML (render array) routes? In other words: we can make JSON API routes have a different default format. This is also what the Symfony FOSRestBundle does: http://stackoverflow.com/a/21805333.

This does not yet pass testing; what's failing here is that \Drupal\jsonapi\EventSubscriber\DefaultExceptionSubscriber is not being used for 4xx responses (e.g. when trying to access an inaccessible entity), because api_json is not the format associated with the route. I think adding something like FOSRestBundle's default_format route option to Drupal core's routing system would make sense, and that would definitely solve it. It'd effectively be a cleaner implementation of exactly the principle that @dawehner proposed in #20: hardcoding a certain format for a certain route.

P.S.: this could even help with #2841027: JSON API format should only be used on JSON API routes, but can currently also be used for REST resources.

Status: Needs review » Needs work

The last submitted patch, 30: 2831137-alt-30.patch, failed testing.

dawehner’s picture

This does not yet pass testing; what's failing here is that \Drupal\jsonapi\EventSubscriber\DefaultExceptionSubscriber is not being used for 4xx responses (e.g. when trying to access an inaccessible entity), because api_json is not the format associated with the route. I think adding something like FOSRestBundle's default_format route option to Drupal core's routing system would make sense, and that would definitely solve it. It'd effectively be a cleaner implementation of exactly the principle that @dawehner proposed in #20: hardcoding a certain format for a certain route.

Well right, my solution simply solves that. The other parts are indeed not hard to solve at all.

Wim Leers’s picture

Indeed. What I'm suggesting (but wasn't expressed clearly enough, sorry about that!) is that we shouldn't add a new route filter … we should add this default_format capability to Drupal's routing system. The default value for it would be html. And the JSON API module would change that to api_json for its routes.

e0ipso’s picture

@Wim Leers what if an error occurs before the route has been matched? How would be fetch the default_format?

dawehner’s picture

Maybe we should simply do both.

we should add this default_format capability to Drupal's routing system.

It is really a question at which point in time you add this information. One question I though want to ask here. Do we have more usecases for this capability? Do we really want people to avoid specifying the format they want to retrieve? In a pure REST world you would always want to specify the accept header, don't you? In our world this would mean to specify _format. In JSONAPI we have the prefix.

The reason I'm a bit resistant is that the Drupal routing system is already utterly complex, and designed for a usecase for its actually not working (see page_manager).

dawehner’s picture

Wim Leers’s picture

what if an error occurs before the route has been matched? How would be fetch the default_format?

Well that's the thing: default_format would cause the format to be set very early. How do we determine request format today?

    public function getRequestFormat($default = 'html')
    {
        if (null === $this->format) {
            $this->format = $this->get('_format', $default);
        }

        return $this->format;
    }

Also today, we need $request->set('_format', 'hal_json') to be called very early for HAL+JSON requests to set correct error responses. This would be no different.

This currently happens in \Drupal\Core\StackMiddleware\NegotiationMiddleware::handle().

Perhaps you meant to ask: how can we ensure that the JSONAPI format is used even before the request has been matched to a JSONAPI route? — the answer to that is that we indeed cannot. But such early failures are problematic (and rare) anyway. The vast majority of failures happen after routing, i.e. during the execution of the controller of the route that was matched to the request.


It is really a question at which point in time you add this information.

Indeed.

Do we really want people to avoid specifying the format they want to retrieve?

How are we doing that?

In a pure REST world you would always want to specify the accept header, don't you?

Yes, but the real world is different: Facebook and GitHub APIs for example use example.com/…/blah.json.

The reason I'm a bit resistant is that the Drupal routing system is already utterly complex

Agreed! And this is why I think that adding default_format is simpler than #20, because it wouldn't require the route filter system to be expanded/refined, and it wouldn't require the addition of a new route filter by the JSON API module. There are other modules that would benefit from this, e.g. https://www.drupal.org/project/acquia_contenthub and https://www.drupal.org/project/tag1quo.

and designed for a usecase for its actually not working (see page_manager).

Huh? Can you elaborate?


Actually, why are we not just simply setting the _format key in defaults? Doesn't that do exactly what I'm writing here?

dawehner’s picture

Yes, but the real world is different: Facebook and GitHub APIs for example use example.com/…/blah.json.

Right, I just wanted to point out that you need some way to explicitly say what you need. Baking that as a generic tool into the routing system itself might be weird, as people could abuse it in a way that the URLs are no longer explicit.

Actually, why are we not just simply setting the _format key in defaults? Doesn't that do exactly what I'm writing here?

It works for 200 responses as well as 403 responses, but it doesn't work for some of the others.

Wim Leers’s picture

In fact, what I wrote in #37 can work easily. We just need a bugfix in core's negotation middleware. It's FAR too eager to set html as the requested format, even when it wasn't requested.

(This uses the fact that Request::getRequestFormat() returns 'html' by default, unless you specify a different default.)

Simple, is it not?

Wim Leers’s picture

Priority: Minor » Normal

We discussed this on today's JSON API call.

So we have three options right now:

  1. In HEAD: ?_format: works always
  2. @dawehner's proposal in #20/#24 with a lazy route filter
  3. my proposal in #39

Point 2 does work for this also works for 404s (e.g. /jsonapi/articlesss will result in a JSON API-formatted 404 response.


@dawehner and @e0ipso preferred my proposal in #39, but before we make a decision there, we need to compare the different approaches (and perhaps additional ones). So let's evaluate it based on these 3 cases:

  1. 404 because of typo
  2. 403 because of route access
  3. basic auth credentials wrong

The expectation is that all three of them use the JSON API exception subscriber. If that's not the case with one of the approaches, then that means we should carefully consider whether that approach is then still acceptable.

Wim Leers’s picture

And while working on evaluation mentioned at the bottom of #40, I encountered yet another bug in core that was getting in the way of the approach shown in #39: #2854560: \Drupal\Core\Routing\RequestFormatRouteFilter::filter() is too HTML-centric.

Wim Leers’s picture

Status: Needs work » Needs review

So, here's the evaluation of those 3 cases listed at the bottom of #39:

  1. 404 because requesting http://d8/jsonapi/taxonomy_term/tagss instead of http://d8/jsonapi/taxonomy_term/tags results in this:
    The website encountered an unexpected error. Please try again later.
    
    Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException: No route found for the specified format html. in Drupal\Core\Routing\RequestFormatRouteFilter->filter() (line 48 of core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php).
    Drupal\Core\Routing\LazyRouteFilter->filter(Object, Object) (Line: 247)
    Drupal\Core\Routing\Router->applyRouteFilters(Object, Object) (Line: 151)
    Drupal\Core\Routing\Router->matchRequest(Object) (Line: 90)
    Drupal\Core\Routing\AccessAwareRouter->matchRequest(Object) (Line: 203)
    Drupal\system\PathBasedBreadcrumbBuilder->getRequestForPath('/jsonapi', Array) (Line: 145)
    Drupal\system\PathBasedBreadcrumbBuilder->build(Object) (Line: 83)
    Drupal\Core\Breadcrumb\BreadcrumbManager->build(Object) (Line: 72)
    Drupal\system\Plugin\Block\SystemBreadcrumbBlock->build() (Line: 203)
    Drupal\block\BlockViewBuilder::preRender(Array)
    call_user_func('Drupal\block\BlockViewBuilder::preRender', Array) (Line: 376)
    Drupal\Core\Render\Renderer->doRender(Array) (Line: 448)
    Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 195)
    Drupal\Core\Render\Renderer->render(Array) (Line: 490)
    Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 97)
    __TwigTemplate_2f32790813595ecc8efd497cb7ec0a5e027860abaefc98f501b957166bd035f4->doDisplay(Array, Array) (Line: 379)
    Twig_Template->displayWithErrorHandling(Array, Array) (Line: 347)
    Twig_Template->display(Array) (Line: 358)
    Twig_Template->render(Array) (Line: 64)
    twig_render_template('core/themes/bartik/templates/page.html.twig', Array) (Line: 384)
    Drupal\Core\Theme\ThemeManager->render('page', Array) (Line: 435)
    Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 195)
    Drupal\Core\Render\Renderer->render(Array) (Line: 490)
    Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 90)
    __TwigTemplate_2479872e0e873dcbd8e108f98025f0816528f5baa7960814bfd1508980248385->doDisplay(Array, Array) (Line: 379)
    Twig_Template->displayWithErrorHandling(Array, Array) (Line: 347)
    Twig_Template->display(Array) (Line: 358)
    Twig_Template->render(Array) (Line: 64)
    twig_render_template('core/themes/classy/templates/layout/html.html.twig', Array) (Line: 384)
    Drupal\Core\Theme\ThemeManager->render('html', Array) (Line: 435)
    Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 195)
    Drupal\Core\Render\Renderer->render(Array) (Line: 147)
    Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 574)
    Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 148)
    Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
    Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) (Line: 111)
    Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('kernel.view', Object) (Line: 144)
    Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 2) (Line: 62)
    Symfony\Component\HttpKernel\HttpKernel->handle(Object, 2, 1) (Line: 57)
    Drupal\Core\StackMiddleware\Session->handle(Object, 2, 1) (Line: 47)
    Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 2, 1) (Line: 99)
    Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 2, 1) (Line: 78)
    Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 2, 1) (Line: 47)
    Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 2, 1) (Line: 52)
    Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 2, 1) (Line: 23)
    Stack\StackedHttpKernel->handle(Object, 2) (Line: 153)
    Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber->makeSubrequest(Object, '/system/404', 404) (Line: 109)
    Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber->on404(Object) (Line: 109)
    Drupal\Core\EventSubscriber\HttpExceptionSubscriberBase->onException(Object, 'kernel.exception', Object) (Line: 111)
    Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('kernel.exception', Object) (Line: 216)
    Symfony\Component\HttpKernel\HttpKernel->handleException(Object, Object, 1) (Line: 70)
    Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
    Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
    Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 207)
    Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 121)
    Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 75)
    Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 47)
    Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 52)
    Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
    Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 656)
    Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
    

    instead of:

    {
      "errors": [
        {
          "title": "Not Found",
          "status": 404,
          "detail": "No route found for \"GET /jsonapi/taxonomy_term/tagss\"",
          "links": {
            "info": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5"
          },
          "code": 0
        }
      ]
    }
    
  2. 403 because of route access, e.g. http://d8/jsonapi/taxonomy_vocabulary/taxonomy_vocabulary:
    {"data":[],"meta":{"errors":[{"title":"Forbidden","status":403,"detail":"The current user is not allowed to GET the selected resource. The \u0027administer taxonomy\u0027 permission is required.","links":{"info":"http:\/\/www.w3.org\/Protocols\/rfc2616\/rfc2616-sec10.html#sec10.4.4"},"code":0,"id":"\/taxonomy_vocabulary--taxonomy_vocabulary\/389a43cd-898a-42be-a8c5-1dee7fa8718b","source":{"pointer":"\/data"}}]},"links":{"self":"http:\/\/d8\/jsonapi\/taxonomy_vocabulary\/taxonomy_vocabulary"}}
    
  3. incorrect basic auth credentials: that doesn't even work correctly with the current JSON API module… :( Try accessing http://d8/jsonapi/taxonomy_vocabulary/taxonomy_vocabulary?_format=api_json with incorrect basic auth, this is the response you get:
    {"data":[],"meta":{"errors":[{"title":"Forbidden","status":403,"detail":"The current user is not allowed to GET the selected resource. The \u0027administer taxonomy\u0027 permission is required.","links":{"info":"http:\/\/www.w3.org\/Protocols\/rfc2616\/rfc2616-sec10.html#sec10.4.4"},"code":0,"id":"\/taxonomy_vocabulary--taxonomy_vocabulary\/389a43cd-898a-42be-a8c5-1dee7fa8718b","source":{"pointer":"\/data"}}]},"links":{"self":"http:\/\/d8\/jsonapi\/taxonomy_vocabulary\/taxonomy_vocabulary"}}
    

    No 401 response as one would expect.


Given the recent addition of the jsonapi.resource_list route that uses the /jsonapi path (introduced in #2790395: Patch file included in commit), I'm somewhat surprised that point 1 is failing like it does. But apparently the D8 Symfony-based router does consider /jsonapi, but then dismisses it because it has number_parts = 1, but the query for /jsonapi/taxonomy_term/tagss does something like WHERE pattern_outline IN (…, '/jsonapi') AND number_parts >= 3.

So we could add the artificial /jsonapi/%, /jsonapi/%/%, /jsonapi/%/%/% and /jsonapi/%/%/%/% routes just for catching typos, and hence serving JSON API-formatted error responses. but that seems to be missing the point too.


  1. As far as I can tell, this means that my proposed approach in #39 does not seem adequate.
  2. Sadly, dawehner's proposal in #20 has the exact same problems :(
  3. This is because both of our approaches use route filters. And route filters only run after successful routing: a route is found (no 404), and access is granted (no 403). For this to work, we also need to ensure 403 and 404s behave as expected, and that will AFAICT require going back to an approach like @e0ipso's in #11: using a middleware.

The last submitted patch, 39: 2831137-alt-core-bugfix.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 39: 2831137-alt-38.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review

Didn't mean to get testbot to test those patches in #39. Meant to set to NR for #43's analysis.

dawehner’s picture

This is because both of our approaches use route filters. And route filters only run after successful routing

Oh right, damnit.

Note: In the PRE 8.3.x router we used to have a event thrown during matching.

Wim Leers’s picture

Title: [PP-1] Assume the use of the 'api_json' format for routes managed by JSON API » [PP-1] Remove the need for ?_format=api_json: assume the use of the 'api_json' format for routes managed by JSON API

Clarifying title.

Wim Leers’s picture

We discussed this on today's JSON API call.

We agreed on the following steps forward:

  1. Set the format using a route filter, as in the patches that @dawehner and I wrote. Probably my approach makes more sense, especially now that #2854560: \Drupal\Core\Routing\RequestFormatRouteFilter::filter() is too HTML-centric is already committed and #2854543: NegotiationMiddleware calls $request->setRequestFormat('html') when there is no _format request parameter, but shouldn't is RTBC. Either way, the route filter approach means that 4xx/500 responses before routing to a JSON API route happened, which includes 404s, but excludes e.g. 403s due to insufficient permissions will result in less-than-great responses.
  2. Support the Accept request header on JSON API routes, which will then ensure that all 4xx/500 responses work fine.

I see now that I already proposed this in #6. And @e0ipso already implemented it in #11. We had a discussion about this in #15, #16, #17 and #18.

We'll need to dig in deeper whether the steps we agreed on actually make sense after all…

e0ipso’s picture

Title: [PP-1] Remove the need for ?_format=api_json: assume the use of the 'api_json' format for routes managed by JSON API » Remove the need for ?_format=api_json: assume the use of the 'api_json' format for routes managed by JSON API
Category: Task » Feature request

The last submitted patch, 30: 2831137-alt-30.patch, failed testing.

e0ipso’s picture

hampercm’s picture

Status: Needs review » Needs work

Great to see progress on this!

  1. +++ b/src/Controller/RequestHandler.php
    @@ -58,7 +59,12 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
    +    if ($format != 'html' && $format !== 'api_json') {
    

    Is keeping this check against 'html' necessary to work around a core issue?

    Also nit-pick: use '!=' vs. '!==' consistently

  2. +++ b/src/Controller/RequestHandler.php
    @@ -102,7 +108,7 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
    +    return $this->renderJsonApiResponse($request, $response, $serializer, 'api_json', $error_handler);
    

    Can the 4th parameter be removed, instead of passing in a hard-coded value?

  3. +++ /dev/null
    @@ -1,100 +0,0 @@
    -  /**
    -   * Tests that JSON API is not supported
    -   */
    -  public function testJsonApiFormatNotSupportedInRest() {
    

    Should we keep this test to verify JSON API isn't breaking core?

e0ipso’s picture

Regarding #53:

  1. Yes, core adds html if no format is provided. There is an issue above that should fix that, we'll need to come back and fix this code if that is ever addressed. Also, I'll fix the inconsistent !=
  2. Good point
  3. This test was somehow duplicated under 2 different names.
e0ipso’s picture

Status: Needs work » Needs review
FileSize
15.37 KB
1.92 KB

This patch also includes a re-roll.

Wim Leers’s picture

Status: Needs review » Needs work
  1. +++ b/src/EventSubscriber/DefaultExceptionSubscriber.php
    @@ -32,8 +33,8 @@ class DefaultExceptionSubscriber extends SerializationDefaultExceptionSubscriber
    -    $format = $event->getRequest()->getRequestFormat();
    -    if ($format !== 'api_json') {
    +    $route = $event->getRequest()->get(RouteObjectInterface::ROUTE_OBJECT);
    +    if (!$route || !$route->getOption('_is_jsonapi')) {
    
    @@ -50,12 +51,13 @@ class DefaultExceptionSubscriber extends SerializationDefaultExceptionSubscriber
    -    $format = $event->getRequest()->getRequestFormat();
    -    if ($format !== 'api_json') {
    +    $route = $event->getRequest()->get(RouteObjectInterface::ROUTE_OBJECT);
    +    if (!$route || !$route->getOption('_is_jsonapi')) {
    

    I don't understand why we're making these changes.

    Can you explain why they are necessary? Which tests do they make pass?

  2. +++ b/src/Routing/Routes.php
    @@ -120,7 +119,6 @@ class Routes implements ContainerInjectionInterface {
    -        ->setRequirement('_format', 'api_json')
    

    Actually, why are we removing these?

    I've been working on core patches to unblock this issue through fixes in core, and they rely on this. See #2854560: \Drupal\Core\Routing\RequestFormatRouteFilter::filter() is too HTML-centric and #2854543: NegotiationMiddleware calls $request->setRequestFormat('html') when there is no _format request parameter, but shouldn't.

  3. +++ b/tests/src/Functional/RestJsonApiUnsupported.php
    @@ -82,20 +82,7 @@ class RestJsonApiUnsupported extends ResourceTestBase {
    -    $expected_body = Json::encode([
    -      'errors' => [
    -        [
    -          'title' => 'Not Acceptable',
    -          'status' => 406,
    -          'detail' => 'Not acceptable format: api_json',
    -          'links' => [
    -            'info' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.7',
    -          ],
    -          'code' => 0,
    -        ],
    -      ],
    -    ]);
    -    $this->assertResourceResponse(406, $expected_body, $response);
    +    $this->assertResourceErrorResponse(406, FALSE, $response);
    

    Why is this changing?

Wim Leers’s picture

e0ipso’s picture

Regarding #56:

  1. It's fixing Drupal\Tests\jsonapi\Functional\JsonApiFunctionalTest. Since we're not setting the request format anywhere, because it's implicit on JSON API routes, we cannot rely on it to format known errors. An alternative would be to have an additional route filter to manually set the format but that is more complex than this particular change. However, setting the format may have added benefits (?).
  2. Past Wim thought it was a good idea (https://www.drupal.org/node/2831137#comment-11918548) I agree with him. I allows matching the route when no _format is passed (and therefore, html is negotiated).
  3. The 406 error happens before the route has been matched. Now that we only know the format on route match, we can't ensure the response having the JSON API compliant payload.
Wim Leers’s picture

1+2: but once #2857793: [PP-1] Add Accept header negotiation for JSON API is in this will change again. Why are we doing #2857793 separately and not as part of this issue? I think it makes it very confusing to understand what the intended end state is.

3: ok.

e0ipso’s picture

Why are we doing #2857793 separately and not as part of this issue?

That was done for the sake of clarity. If you think it's clearer to have a combined issue, I'll merge the code in here.

Wim Leers’s picture

I think it'll be clearer to have both in this issue, yes.

e0ipso’s picture

e0ipso’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 62: 2831137--drop-required-format--62.patch, failed testing.

dawehner’s picture

  1. +++ b/jsonapi.services.yml
    @@ -99,3 +99,7 @@ services:
    +  http_middleware.format_setter:
    

    IMHO prefixing the service name with jsonapi is a good idea.

  2. +++ b/jsonapi.services.yml
    @@ -99,3 +99,7 @@ services:
    +    tags:
    +      - { name: http_middleware, priority: 399, responder: true }
    

    Can we document why we use this specific priority?

  3. +++ b/src/EventSubscriber/DefaultExceptionSubscriber.php
    @@ -61,4 +60,21 @@ class DefaultExceptionSubscriber extends SerializationDefaultExceptionSubscriber
    +    $route = $request->get(RouteObjectInterface::ROUTE_OBJECT);
    

    IMHO we should use $request->attributes->get() here. I hope we don't pass along the route object as query parameter / via a POST body.

  4. +++ b/src/StackMiddleware/FormatSetter.php
    @@ -0,0 +1,45 @@
    +    $accept = $request->headers->get('Accept');
    +    if (strpos($accept, 'application/vnd.api+json') !== FALSE) {
    

    For a little bit better code you could leverage: Request::getAcceptableContentTypes()

gaurav.kapoor’s picture

Assigned: Unassigned » gaurav.kapoor
gaurav.kapoor’s picture

Assigned: gaurav.kapoor » Unassigned
dawehner’s picture

+++ b/src/Access/CustomQueryParameterNamesAccessCheck.php
@@ -25,7 +25,10 @@ class CustomQueryParameterNamesAccessCheck implements AccessInterface {
+  public function access(Request $request = NULL) {
+    if (!$request) {
+      return AccessResult::neutral();
+    }

You should be able to set 'use_incoming_request', see core.services.yml

e0ipso’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 69: 2831137--drop-required-format--69.patch, failed testing.

dawehner’s picture

Looks great now! I assume it passes in your head already.

e0ipso’s picture

e0ipso’s picture

Status: Needs work » Needs review
e0ipso’s picture

I'm trying to nail down the priority of the format setter.

e0ipso’s picture

FileSize
709 bytes

Added the correct interdiff.

The last submitted patch, 73: 2831137--drop-required-format--71.patch, failed testing.

  • e0ipso committed 84c0ba0 on 8.x-1.x
    fix(DX): Remove the need for ?_format: assume the format for routes...
e0ipso’s picture

Status: Needs review » Fixed

So glad about this one! Thanks all.

dawehner’s picture

Yeah! It is really cool that this particular issue landed.

Wim Leers’s picture

Yay, glad this is in! And hurray, we now have a beta-1 release! :D

I have lots of nits though.

  1. +++ b/jsonapi.services.yml
    @@ -99,6 +99,12 @@ services:
    +    # Set priority to 201 so it happens right before the page cache middleware
    +    # has the opportunity to respond. http_middleware.page_cache is 200.
    

    Indentation is wrong.

  2. +++ b/jsonapi.services.yml
    @@ -99,6 +99,12 @@ services:
    +      - { name: http_middleware, priority: 201, responder: true }
    

    I don't think this needs responder: true.

    Quoting \Drupal\Core\DependencyInjection\Compiler\StackedKernelPass:

     * The 'http_middleware' service tag additionally accepts a 'responder'
     * parameter. It should be set to TRUE if many or most requests will be handled
     * directly by the middleware.
    
  3. +++ b/src/Controller/RequestHandler.php
    @@ -11,6 +11,7 @@ use Symfony\Component\DependencyInjection\ContainerAwareTrait;
    +use Symfony\Component\HttpKernel\Exception\HttpException;
    

    Sole change in this file, hence unnecessary.

  4. +++ b/src/EventSubscriber/DefaultExceptionSubscriber.php
    @@ -50,13 +51,30 @@ class DefaultExceptionSubscriber extends SerializationDefaultExceptionSubscriber
    +    $response->headers->set('content-type', 'application/vnd.api+json');
    

    Nit: Content-Type.

  5. +++ b/src/EventSubscriber/DefaultExceptionSubscriber.php
    @@ -50,13 +51,30 @@ class DefaultExceptionSubscriber extends SerializationDefaultExceptionSubscriber
    +    return $format == 'api_json' || ($route && $route->getOption('_is_jsonapi'));
    

    ===

  6. +++ b/src/StackMiddleware/FormatSetter.php
    @@ -0,0 +1,47 @@
    + * If the request belongs to a JSON API managed route, then sets the api_json
    + * format manually.
    

    Violates 80 cols rule. More importantly: it's completely wrong. That's not what this does. This only inspects the Accept header. It doesn't check that this is a JSON API route. And AFAICT that is very dangerous. Because it means we've re-introduced Accept header-based negotiation. Only a limited one. But it means you can force a JSON API response on any route. Fortunately it looks like this can't cause much damage, since requesting e.g. a HTML-only route with Accept: application/vnd.api+json results in a 406 error.
    But still, why not force this to be limited to requests that at least also contain /jsonapi in their request path? That may be imperfect, but reduces the problem surface area until we can find a more precise solution.

  7. +++ b/src/StackMiddleware/FormatSetter.php
    @@ -0,0 +1,47 @@
    +   * Constructs a PageCache object.
    

    C/P remnant.

  8. +++ b/src/StackMiddleware/FormatSetter.php
    @@ -0,0 +1,47 @@
    +    // Check if the accept header is set to the official header.
    

    s/accept/'Accept'/
    s/official header/JSON API MIME type/

  9. +++ b/src/StackMiddleware/FormatSetter.php
    @@ -0,0 +1,47 @@
    +      return strpos($accept, 'application/vnd.api+json') !== FALSE;
    

    Wouldn't === 0 be more logical?

  10. +++ b/src/StackMiddleware/FormatSetter.php
    @@ -0,0 +1,47 @@
    +      // Manually set the format.
    

    Pointless comment.

  11. +++ b/tests/src/Functional/JsonApiFunctionalTest.php
    @@ -244,6 +244,21 @@ class JsonApiFunctionalTest extends JsonApiFunctionalTestBase {
    +    // 19. Test non-existing route without negotiation header.
    ...
    +    // 20. Test non-existing route with negotiation header.
    

    s/negotiation/'Accept'/

  12. +++ b/tests/src/Functional/JsonApiFunctionalTest.php
    @@ -244,6 +244,21 @@ class JsonApiFunctionalTest extends JsonApiFunctionalTestBase {
    +    // Without the Accept header we cannot know we want the 404 error formatted
    ...
    +    // With the Accept header we can know we want the 404 error formatted as
    

    s/Accept/'Accept'/

Patch attached. I'm happy to move this to a new issue if you prefer that.

Wim Leers’s picture

Status: Fixed » Needs review
e0ipso’s picture

Status: Needs review » Needs work

Thanks for the follow up Wim. One minor change:

+++ b/src/StackMiddleware/FormatSetter.php
@@ -32,16 +31,32 @@ class FormatSetter implements HttpKernelInterface {
+    return strpos($request->getPathInfo(), '/jsonapi') !== FALSE;

Maybe check for /jsonapi/ or /\/jsonapi$/. It may be a bit more accurate and rule out things like /blog/2017/jsonapi-makes-my-dinner.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
656 bytes
6.86 KB

Riiiiight, good call!

dawehner’s picture

  1. +++ b/jsonapi.services.yml
    @@ -102,9 +102,9 @@ services:
    -    # Set priority to 201 so it happens right before the page cache middleware
    -    # has the opportunity to respond. http_middleware.page_cache is 200.
    -      - { name: http_middleware, priority: 201, responder: true }
    +      # Set priority to 201 so it happens right before the page cache
    +      # middleware (priority 200)has the opportunity to respond.
    +      - { name: http_middleware, priority: 201 }
     
    

    +1 for this change. This ensures that the middleware is not marked as lazy, even technically it will run all the time. Therefore by not marking it as "responder" there is a bit of time shaved off.

  2. +++ b/src/StackMiddleware/FormatSetter.php
    @@ -32,16 +31,32 @@ class FormatSetter implements HttpKernelInterface {
    +    if (static::hasJsonApiRequestPath($request)) {
    +      // Check if the 'Accept' header includes the JSON API MIME type.
    +      $content_types = array_filter($request->getAcceptableContentTypes(), function ($accept) {
    +        return strpos($accept, 'application/vnd.api+json') === 0;
    +      });
    +      if (count($content_types)) {
    +        $request->setRequestFormat('api_json');
    +      }
    

    I'm wondering whether a better method would be: static::>isJsonApiRequest()

Wim Leers’s picture

#85.2: haha I thought about exactly that, I first used is, then switched to has. Honestly, it doesn't matter, because this is protected: it's not an API. So, I'd say: @e0ipso, you can choose :)

gaurav.kapoor’s picture

Status: Needs review » Needs work

The last submitted patch, 87: 2831137-followup-87.patch, failed testing.

dawehner’s picture

#85.2: haha I thought about exactly that, I first used is, then switched to has. Honestly, it doesn't matter, because this is protected: it's not an API. So, I'd say: @e0ipso, you can choose :)

Well, my primary point was to actually include more into this method. Wouldn't it be nice if the final code looks like:

if (static::isJsonapiRequest($request)) {
  $request->setFormat('api_json');
}
Wim Leers’s picture

Status: Needs work » Needs review
FileSize
2.2 KB
6.82 KB

#89: Ah, yes, of course. That'd be much nicer. Done.

#87: I ignored your patch… because you didn't provide an interdiff, nor a comment, so I have no idea what changed. Plus, my patch in #84 passed, yours did not.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Nice!

gaurav.kapoor’s picture

I wanted to Test it with method call isJsonApiRequest() in #87. Why do test failure don't provide the exact reason for failure?

Wim Leers’s picture

#92: they do, if you click the "4 fail" red link: https://www.drupal.org/pift-ci-job/620758.

gaurav.kapoor’s picture

That i know , but even in that link i am not very convinced with the failure messages. They aren't guiding towards any solution.

tormi’s picture

Documentation needs some updates, too ;)

tormi’s picture

Removed _format=api_json from documentation examples.

  • e0ipso committed 1885d1a on 8.x-1.x authored by Wim Leers
    fix(Maintainability) Clean up format negotiation and heuristic match on...
e0ipso’s picture

Status: Reviewed & tested by the community » Fixed

Back to fixed! Thanks all.

Status: Fixed » Closed (fixed)

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