Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
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'],
]);
}
}
}
Comment | File | Size | Author |
---|---|---|---|
#90 | 2831137-followup-90.patch | 6.82 KB | Wim Leers |
| |||
#76 | 2831137--interdiff--73-75.txt | 709 bytes | e0ipso |
#75 | 2831137--drop-required-format--75.patch | 18.13 KB | e0ipso |
#73 | 2831137--drop-required-format--71.patch | 18.13 KB | e0ipso |
#73 | 2831137--interdiff--69-71.txt | 2.04 KB | e0ipso |
Comments
Comment #2
Wim LeersComment #3
skyredwangThe JSONAPI specification states:
vnd.
indicates "media types associated with publicly available products" (link). Here the publicly available product isapi+json
.The character "+" needs to be escaped when used in URL. Therefore, changing it to "_"(underscore) makes it easier to be recognized.
Comment #4
Wim LeersI know that. But it's
jsonapi.org
and thejsonapi
module et cetera. Soapi_json
makes less sense to me thanjsonapi
.Comment #5
e0ipsoWe are breaking the specification by not negotiating formats using the standard way (
Accept
header) in Drupal. We should be usingapplication/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 tohal_json
.Comment #6
Wim LeersI 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 thanContent-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.Works for me. Then that should just be documented :)
Comment #7
e0ipsoWould it set the expectation that you can do the same with other formats in core?
Comment #8
Wim LeersNo, 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?
Comment #9
e0ipsoThe 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?
Comment #10
hampercm CreditAttribution: hampercm at Acquia commented+1 for dropping the need to specify
_format=api_json
on the JSON API module's routesComment #11
e0ipsoThe following patch adds support for the Accept header, and is BC. That means that you can use the
_format
drupalism or theAccept:
spec compliant for the format negotiation.Comment #12
e0ipsoComment #13
e0ipsow00t! Tests passing on the first try. That's new.
Comment #14
Wim LeersReviewing!
Comment #15
hampercm CreditAttribution: hampercm at Acquia commentedOne 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.Comment #16
e0ipsoThat 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:
Accept
header and profit._format
alternative.Thoughts?
Comment #17
Wim LeersThis 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.
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.Using this cache context is a big problem: the
Accept
header may contain any value.Use the
request_format
cache context instead.!== FALSE
should be=== 0
, otherwise evenAccept: text/html, application/vnd.api+json
would match.Comment #18
e0ipsoThis 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:
Comment #19
e0ipsoComment #20
dawehnerSo 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
Comment #21
e0ipsoMaking tests fail.
Comment #24
dawehnerThis time with the actual files ...
Comment #26
e0ipsoComment #27
dawehnerComment #28
e0ipsoAdding 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?Comment #29
dawehnerCan't agree more ...
Comment #30
Wim Leers#28+#29: agreed!
But I honestly wonder why it can't be done in a much simpler way.
We already have
This means the
api_json
format is the only one supported, and that users must specify this format in theirAccept
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), becauseapi_json
is not the format associated with the route. I think adding something like FOSRestBundle'sdefault_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.
Comment #32
dawehnerWell right, my solution simply solves that. The other parts are indeed not hard to solve at all.
Comment #33
Wim LeersIndeed. 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 behtml
. And the JSON API module would change that toapi_json
for its routes.Comment #34
e0ipso@Wim Leers what if an error occurs before the route has been matched? How would be fetch the
default_format
?Comment #35
dawehnerMaybe we should simply do both.
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).
Comment #36
dawehnerHere is a related issue #2821701: Default '_format' for REST resources routes
Comment #37
Wim LeersWell that's the thing:
default_format
would cause the format to be set very early. How do we determine request format today?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:
— 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.Indeed.
How are we doing that?
Yes, but the real world is different: Facebook and GitHub APIs for example use
example.com/…/blah.json
.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.Huh? Can you elaborate?
Actually, why are we not just simply setting the
_format
key indefaults
? Doesn't that do exactly what I'm writing here?Comment #38
dawehnerRight, 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.
It works for 200 responses as well as 403 responses, but it doesn't work for some of the others.
Comment #39
Wim LeersIn 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?
Comment #40
Wim LeersWe discussed this on today's JSON API call.
So we have three options right now:
?_format
: works alwaysPoint 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:
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.
Comment #41
Wim LeersI worked on getting #39's core patch into core: #2854543: NegotiationMiddleware calls $request->setRequestFormat('html') when there is no _format request parameter, but shouldn't.
Comment #42
Wim LeersAnd 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.
Comment #43
Wim LeersSo, here's the evaluation of those 3 cases listed at the bottom of #39:
http://d8/jsonapi/taxonomy_term/tagss
instead ofhttp://d8/jsonapi/taxonomy_term/tags
results in this:instead of:
http://d8/jsonapi/taxonomy_vocabulary/taxonomy_vocabulary
:http://d8/jsonapi/taxonomy_vocabulary/taxonomy_vocabulary?_format=api_json
with incorrect basic auth, this is the response you get: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 hasnumber_parts = 1
, but the query for/jsonapi/taxonomy_term/tagss
does something likeWHERE 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.Comment #46
Wim LeersDidn't mean to get testbot to test those patches in #39. Meant to set to NR for #43's analysis.
Comment #47
dawehnerOh right, damnit.
Note: In the PRE 8.3.x router we used to have a event thrown during matching.
Comment #48
Wim LeersClarifying title.
Comment #49
Wim LeersWe discussed this on today's JSON API call.
We agreed on the following steps forward:
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…
Comment #50
e0ipsoComment #52
e0ipsoThis is the approach we agreed upon on the weekly sync. I created #2857793: [PP-1] Add Accept header negotiation for JSON API to get header negotiation in place.
Comment #53
hampercm CreditAttribution: hampercm at Acquia commentedGreat to see progress on this!
Is keeping this check against 'html' necessary to work around a core issue?
Also nit-pick: use '!=' vs. '!==' consistently
Can the 4th parameter be removed, instead of passing in a hard-coded value?
Should we keep this test to verify JSON API isn't breaking core?
Comment #54
e0ipsoRegarding #53:
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!=
Comment #55
e0ipsoThis patch also includes a re-roll.
Comment #56
Wim LeersI don't understand why we're making these changes.
Can you explain why they are necessary? Which tests do they make pass?
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.
Why is this changing?
Comment #57
Wim LeersComment #58
e0ipsoRegarding #56:
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 (?).Comment #59
Wim Leers1+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.
Comment #60
e0ipsoThat 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.
Comment #61
Wim LeersI think it'll be clearer to have both in this issue, yes.
Comment #62
e0ipsoMerging #2857793: [PP-1] Add Accept header negotiation for JSON API into this patch.
Comment #63
e0ipsoComment #65
dawehnerIMHO prefixing the service name with jsonapi is a good idea.
Can we document why we use this specific priority?
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.For a little bit better code you could leverage:
Request::getAcceptableContentTypes()
Comment #66
gaurav.kapoor CreditAttribution: gaurav.kapoor at OpenSense Labs commentedComment #67
gaurav.kapoor CreditAttribution: gaurav.kapoor at OpenSense Labs commentedComment #68
dawehnerYou should be able to set 'use_incoming_request', see
core.services.yml
Comment #69
e0ipsoComment #70
e0ipsoComment #72
dawehnerLooks great now! I assume it passes in your head already.
Comment #73
e0ipsoFixing the test.
Comment #74
e0ipsoComment #75
e0ipsoI'm trying to nail down the priority of the format setter.
Comment #76
e0ipsoAdded the correct interdiff.
Comment #79
e0ipsoSo glad about this one! Thanks all.
Comment #80
dawehnerYeah! It is really cool that this particular issue landed.
Comment #81
Wim LeersYay, glad this is in! And hurray, we now have a beta-1 release! :D
I have lots of nits though.
Indentation is wrong.
I don't think this needs
responder: true
.Quoting
\Drupal\Core\DependencyInjection\Compiler\StackedKernelPass
:Sole change in this file, hence unnecessary.
Nit:
Content-Type
.===
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-introducedAccept
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 withAccept: 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.C/P remnant.
s/accept/'Accept'/
s/official header/JSON API MIME type/
Wouldn't
=== 0
be more logical?Pointless comment.
s/negotiation/'Accept'/
s/Accept/'Accept'/
Patch attached. I'm happy to move this to a new issue if you prefer that.
Comment #82
Wim LeersComment #83
e0ipsoThanks for the follow up Wim. One minor change:
Maybe check for
/jsonapi/
or/\/jsonapi$/
. It may be a bit more accurate and rule out things like/blog/2017/jsonapi-makes-my-dinner
.Comment #84
Wim LeersRiiiiight, good call!
Comment #85
dawehner+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.
I'm wondering whether a better method would be:
static::>isJsonApiRequest()
Comment #86
Wim Leers#85.2: haha I thought about exactly that, I first used
is
, then switched tohas
. Honestly, it doesn't matter, because this is protected: it's not an API. So, I'd say: @e0ipso, you can choose :)Comment #87
gaurav.kapoor CreditAttribution: gaurav.kapoor at OpenSense Labs commentedComment #89
dawehnerWell, my primary point was to actually include more into this method. Wouldn't it be nice if the final code looks like:
Comment #90
Wim Leers#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.
Comment #91
dawehnerNice!
Comment #92
gaurav.kapoor CreditAttribution: gaurav.kapoor at OpenSense Labs commentedI wanted to Test it with method call isJsonApiRequest() in #87. Why do test failure don't provide the exact reason for failure?
Comment #93
Wim Leers#92: they do, if you click the "4 fail" red link: https://www.drupal.org/pift-ci-job/620758.
Comment #94
gaurav.kapoor CreditAttribution: gaurav.kapoor at OpenSense Labs commentedThat i know , but even in that link i am not very convinced with the failure messages. They aren't guiding towards any solution.
Comment #95
tormiDocumentation needs some updates, too ;)
Comment #96
tormiRemoved
_format=api_json
from documentation examples.Comment #98
e0ipsoBack to fixed! Thanks all.