Proposed commit message:
Issue #2331919 by dawehner, Crell, rpayanm, Fabianx | damiankloip: Implement StackNegotiation.
Problem/Motivation
Once #2303673: Implement stackphp; cleanup handlePageCache() and preHandle() is in, we can utilise the clean decorator architecture that StackPhp provides. Content negotiation has been something in the back of people's minds for a while now...
Our current content negotiation is very primitive, and does not handle weighting in the Accept header at all. It also is done, well, wrong. We re-negotiate every time we need the format rather than leveraging the property on the Request object that is designed specifically to hold the result of negotiation. That means $request->getRequestFormat() works sometimes, but not all the time. In a word, it all sucks.
Proposed resolution
Instead, use a stand-alone 3rd party library that handles all the negotiation complexity, and is also used by Symfony REST Edition. It's well-tested and well-used. It also works as a stack middleware, which means negotiation happens before any Drupal happens. That makes $request->getRequestFormat() always the "correct" way to get the request format, and reliably so.
- Use the library https://github.com/willdurand/StackNegotiation to the set request format
- This library is used because its an actual full implementation, see IS of #1505080: [META] Content Negotiation for Accept Headers for other examples, but many of them for example
weren't implemented directly for symfony HTTP foundation. - Another advantage is that there is already a stack php integration available, so we don't have to add anything
- The code coverage of
willdurand/Negotationandwilldurand/StackNegotationis nearly 100%, see attachments - Implement a wrapper for taking into account browser bugs
Clarification:
- This uses willdurand/StackNegotation 0.1.0 - this is a lightweight wrapper (that has around 100 loc - excluding tests) providing the stack integration.
- This uses willdurand/Negotation 1.3.x - this is the stable negotiation library, which implements the RFC for Accept-header negotiation.
Remaining tasks
Commit it!
User interface changes
None
API changes
$request->getRequestFormat() is now reliable, and is what you should use. (It only sometimes worked before.)
The ContentNegotiation class no longer exists, but the use cases for that outside of routing itself are extremely small so I doubt any existing modules will be impacted.
| Comment | File | Size | Author |
|---|---|---|---|
| #117 | coverage.tar_.gz | 274.84 KB | dawehner |
| #117 | coverage.tar_.gz | 181.59 KB | dawehner |
| #99 | 2331919-99.patch | 125.25 KB | dawehner |
| #99 | 2331919-do-not-test.patch | 3.28 KB | dawehner |
| #88 | 2331919-88.patch | 123.81 KB | rpayanm |
Comments
Comment #1
larowlanComment #2
larowlanCrell asked to leave this parked
Comment #3
Crell commentedActually this one is fine. It's the old meta I meant to leave parked. :-) Working on this now.
Comment #4
Crell commentedHere we are. Let's see how well this works.
This includes a new composer library so please look at the do-not-review patch.
Comment #6
Crell commentedAnd of course I find one bug right after I post those...
Comment #8
Crell commentedI really need to stop writing code after midnight. It's like a gremlin. Nothing good comes of it.
Comment #10
Crell commentedOK, this just installed for me locally. Why is it complaining about missing files?
Comment #11
dawehnerI don't see nothing problematic, beside that fact that you forgot to add the actual code.
This is really cool!
this is so much more readable...
This indeed also improves the DX
Comment #12
Crell commentedWith dawehner's help I managed to figure it out. I had referenced a dev version of the library so it installed with a .git directory in it, despite our composer.json file saying to prefer dist. That of course screwed up git entirely. I checked and the current 1.0-dev is identical to the 0.1.0 tag, so I switched to that instead and it should be better now.
No interdiff because I had to do some fierce rebasing to sort it all out, which makes it hard to recreate an interdiff. The only change is the version number specified for the stack-negotiation library, though.
Comment #13
Crell commentedComment #15
Crell commentedHa! language module was a bad boy and didn't prefix a service name. Oh well.
Comment #17
Crell commentedThis should take care of caching. I'm not sure what's going on with the REST tests...
Comment #19
dawehner@crell
Did you really considered this to be easy?
Comment #21
Crell commented406 or 404, pick one. :-)
Also as discussed in IRC today: To handle broken browsers, we'll add another middleware that fires after the Negotiation one. All it does is check for a missing _format and set it to HTML, on the assumption that only a busted browser would send an unmanageable Accept header and busted browsers probably want HTML anyway. (That's roughly what the assumption was in D7, too.)
Comment #22
Crell commentedComment #23
dawehnerComment #24
Crell commentedCan we have a do-not-test without the library itself?
Comment #25
dawehnerComment #26
fabianx commentedI believe this should be coming _before_ checking for custom headers so that it is a true fallback.
Also:
Reviewing the negotiation code I see that all our fallback browsers have */*, so should it not be simpler to add a list of priorities?
e.g. array('html', 'json', 'hal')
or calling getBest() again with some hardcoded priorities?
Because in case of a wildcard match */* the negotiator would by default use the first priority format that is not */*.
As far as I can see the negotiator should still match if a browser explicitly asks for application/json, etc.
But that would need testing.
I am not opposed to the decorator - though I think the upstream interface is misleading as it has too less functions compared to what FormatNegotiator actually implements as public. We should fix that upstream.
Comment #27
fabianx commentedI just re-read the comment for the with-library patch and I think we can disregard my previous comment.
Can we just change this comment then to not speak of a fallback, but as a special case or such?
And fix the upstream interface to match?
Comment #28
dawehner@Fabianx
You are right the comment is wrong and is coming from an earlier implementation.
We do have an issue upstream already https://github.com/willdurand/Negotiation/issues/34 but yeah discussions are a bit difficult.
Comment #29
dawehnerDo you still have some questions? Feel free to ask.
Adapted the comment.
Comment #30
fabianx commented> We do have an issue upstream already https://github.com/willdurand/Negotiation/issues/34 but yeah discussions are a bit difficult.
My main concern was that the BrowserBugsFormatNegotiator implements the FormatNegotiatorInterface, but that has just 4 functions, while BrowserBugsFormatNegotiator and FormatNegotiator have 8 public functions.
So the interface is lacking upstream, which could be a problem in the future if something changes in the FormatNegotiator class, where we don't have a contract.
Alternatively we could just implement the contracted interface and add magic __call method to pass through all other function calls to the class (true wildcard decorator).
The patch here is fine though :).
Lets discuss what we can do about the interface and then this is RTBC :).
Comment #31
dawehner@Fabianx
I see what you mean, thought you have to take into account that the Negotiation stack class takes
FormatNegotiatorInterface $formatNegotiator = nullso we should implement that interface. But you are right, we should call all methods on the decorator class, you never know.
Comment #32
fabianx commentedRTBC - Nice work! - Maybe add another do-not-test.patch so its easier for reviewers to see the relevant changes?
Comment #33
dawehnerSure.
Comment #34
fabianx commentednit - Should this be BrowserBugsFormatNegotiator?
Comment #35
dawehnerSure.
Comment #36
fabianx commentednit - + * Contains \Drupal\Core\StackMiddleware\BrowserBugsFormatNegotiato.
Lets add the r :).
Comment #37
dawehnerMeh^2
Comment #38
Crell commentedNote to committers: While I'm super +1 on this, it is going to conflict with #2323759: Modularize kernel exception handling. Since that one removes code that this patch modifies (and thus would not need to modify anymore) I think it's better to commit that one first, then this one after a (hopefully easy) reroll.
Comment #39
alexpott#2323759: Modularize kernel exception handling has landed
Comment #40
dawehnerHopefully a good reroll. remove a couple of old referencs and the ContentNegotation class itself.
Comment #42
dawehnerComment #44
Crell commentedIncorrect mime type. It's application/hal+json
This is always true if you're using the base class properly.
Use the base class's abstract method, Luke!
And then onHalJson() should use the on403, etc. method names. If you don't want to do that, don't use the base subscriber class. It's either/or.
Doesn't getMimeType() already honor the result of the negotiation process...?
Comment #45
dawehnerThis was actually was I intended to use.
Yeah, it is a shame, you could argue that Request::setFormat() should be called from inside the negotiation.
Comment #47
gábor hojtsySo @Crell said on #2135829-12: EntityResource: translations support that this would make us remove the HTTP language negotiator specific to Drupal. I don't see that being done on the patch so I guess that was planned to be a followup? I am wondering if Drupal 8's extent of working with the Accept-Language header was taken into account when thinking about this. One of the improvements of Drupal 8 over Drupal 7 is now Drupal 8 understands that the language codes Drupal is using may or may not be what language codes an arbitrary browser would send back as a language the client wants. So Drupal 8 has this UI configurable mapping system where we ship with several default mappings and you can add more as needed. This was especially pushed forward by some Chinese users where the language codes are really various: http://cgit.drupalcode.org/drupal/tree/core/modules/language/config/inst...
So long as this system cannot reproduce that, it can stay along as yet one more system doing language negotiation, but it will not be able to replace the Drupal 8 language negotiation system.
Comment #48
gábor hojtsyBTW for background on the language mapping system in our negotiation, see #365615: Language detection not working correctly for most Chinese readers (and add a user interface for all browser language mappings).
Comment #49
dawehnerStumbled upon some fun issue during day to day work, thank you @webflo :(
@Gábor Hojtsy
No, this issue doesn't deal directly with it, though it adds support that you can fetch it, if you like. Do you think the issue above should leverage the information added by this issue?
Comment #50
dawehnerOkay, let's keep the hal subscriber for now.
Comment #51
gábor hojtsy@dawehner: I think the language negotiation part of this 3rd party lib / patch will be useful once it passes all our existing language negotiation tests, which are results of lots of battle testing, and got huge improvements in Drupal 8 even compared to Drupal 7. (Including the configurable language mappings). Until then, it is duplicate functionality that we'll not use. I sense there may be lots of upstream contribution coming out of getting our learnings up to the 3rd party lib. I will unfortunately not be able to devote time to that, its not personal just too much on my plate :/
Comment #52
dawehner@Gábor Hojtsy
Just to be clear, this is just about language negotiation for REST requests where you kind of expect to be able to use the headers.
Obviously, this method doesn't work on any normal HTML request.
Noone forces you to put work on it, but at least for the REST issue, this is the way to do it.
Comment #53
gábor hojtsy@dawehner: are REST clients to be believed to be more consistent than browsers and not use eg. various language codes for Chinese, and instead exactly the codes that Drupal happened to pick a handful of years ago? In other words, is it ok to expect REST clients to need to use the exact internal language code representations of Drupal rather than some other totally valid language code according to the internet standards?
Comment #54
Crell commenteddawehner: Can we have a -do-not-test for review?
Gabor: To the point of this issue, this will have a side effect of the _accept_language request attribute being set based on a reliable HTTP-only negotiation algorithm. Whether that string is useful or not to you is a task for a separate issue, as it doesn't do any validation on the language string itself as far as I'm aware. Use it, don't, that's irrelevant to this thread which is more interested in the Accept header than Language header.
Comment #55
gábor hojtsy@Crell: two days ago on #2135829-12: EntityResource: translations support you wrote
This was the reason I came here. I wanted to point out this does not let us to remove our own HTTP language negotiation or make it wrap the value that this patch produces at least in the current state of the library included. Since in the other thread you said this value will be useful, I thought its worth pointing out that it will not be. If it is not even the goal of this issue to have a useful accept language value for now, that is totally fine, we'll just not use that value then. Unfortunately the goal is/was not clear from the summary, so I went after what you indicated in your comment on that other issue.
Comment #56
Crell commentedI thought it would be. I wasn't aware that there's a dozen non-standard ways to define "Chinese". :-) (And I was trying to explain dawehner's comment in that thread; I think that whole line was a tangent. Sorry for the distraction.)
Comment #57
dawehnerSure.
Comment #58
Crell commentedLet's do this!
Comment #60
fabianx commentedBack to RTBC then, we can always improve the negotiation handling further for i18n.
Comment #63
dawehnerRerolled, single bit in
AccessHeaderMatcher.phpComment #65
dawehnerThere we go.
Comment #66
Crell commentedOne more time...
Comment #68
dawehnerReroll
Comment #69
Crell commentedThis seems impressively fragile, so...
Comment #70
webchickThis is a new lib so AFAIK is Dries's.
Comment #71
webchickOff-hand though:
Um. :) We are planning to ship a product used by governments, companies, etc. all over the world for hugely important, high-traffic sites. Any idea what this means, and what the path to 1.0.0 is? :)
Comment #72
webchickAlso this would be easier to commit if the issue summary contained information on what this buys us, and what the consequence of not committing it is.
Comment #73
Crell commentedAs with Drupal modules, something being not-1.0 means pretty much nothing. There are Drupal modules used by over 100,000 sites that have never had a non-beta release. 1.0 of that lib has only a dev release at the moment, which I wanted to avoid (in part because composer pulled in a git repo for it). I didn't see any meaningful differences between the tag and -dev version before.
Tweaked issue summary, too.
Comment #76
klausiThis looks like a very good improvement. Having a properly setup Request object with an already negotiated content format simplifies a lot of code dealing with that. Some minor problems:
This needs some more docs. Why do we need this? What is this?
So you want to hijack all method calls? Please add a comment. And I think you mean $this->formatNegotiator here? Do we actually need this?
Why do we even have to invoke the content negotiation here? Just set the format on the request and be done with it for the exercise of this test? It should not also test the content negotiation, that would be a different unit test.
The variable $accept_header is never used afterwards?
2 statements ending with ";" on one line?
Comment #77
dawehnerImproved the documentation, feel free to give some feedback on it.
Yeah this was pretty much pointless, as yeah, even the implementation was broken, tbh.
Good point.
Yeah this happened on accident.
Comment #78
dawehnerLet's reapply.
Comment #81
dries commentedThis patch seems to make things more maintainable and fixes a bug.
Based on the policy outlined in #2350615: [policy, no patch] What changes can be accepted during the Drupal 8 beta phase?, this issue was worked on during DrupalCon Amsterdam, so if it is ready by Oct. 28, we will consider it for inclusion.
Otherwise, we need to evaluate whether this issue is in fact fixing a major bug or other important stabilizations listed in that policy, we will need to evaluate the impact versus the disruption. So, let's add clarification to the issue summary regarding what specific bugs this may actually be solving, including test coverage for them, and reference any related issues.
Comment #82
xjmPer @effulgentsia, a related bug was raised in #1505080-40: [META] Content Negotiation for Accept Headers. Should we file an explicit bug report for that?
Does this also provide a way to resolve #2339491: Ajax requests on forms with files fail on IE 9?
Comment #83
dawehner@Dries, @xjm
There are several points:
After this issue I'll probably do some research on that one.
Should we file an explicit bug report for that?If someone wants to invest the time and aggregate the information from issues like #2026431: Make ContentNegotiation a "internal" service, used only by the router, so that core or contrib can implement real negotiation have fun, but I just thinkthat there are better ways to invest our time.
Another reroll, note: This patch was once RTBC a month ago already.
Let's drop that silly tag if it doesn't help anything anyway :)
Comment #84
Crell commentedAs already noted in the issue summary the API impact here is extremely small. It's very unlikely that any existing modules would be impacted, and even if they are the change is trivial. (You now can always rely on $request->getRequestFormat(), whereas before you only sometimes could so had to use our crappy old Conneg class manually.)
This has been RTBC before I think it's safe to re-RTBC with tests passing.
Comment #85
fabianx commentedPatch is still RTBC
Comment #87
fabianx commentedComment #88
rpayanmhere the reroll.
Comment #89
Crell commentedOnce more...
Comment #90
chx commentedFascinating: the word performance does not even appear in this issue. Although usually Drupal 8 doesn't care much about performance hoping that caching will take care of everything, this is way too early for that.
Comment #91
dawehner@chx
I really hope we use the same requirements for all issues around ... working on a benchmark now.
Comment #92
dawehnerRun without page caching
http://www.lionsad.de/xhprof-kit/xhprof/xhprof_html/?run1=5446d0de206d8&...
Page caching enabled
Run 5446d71499e7c uploaded successfully for drupal-perf-dawehner.
Run 5446d73a3daba uploaded successfully for drupal-perf-dawehner.
http://www.lionsad.de/xhprof-kit/xhprof/xhprof_html/?run1=5446d71499e7c&...
Comment #93
fabianx commentedSomething looks strange about this benchmarks. missing ct is 'interesting'. Maybe a uprofiler incompatibility?
Comment #94
effulgentsia commentedIf the profiling numbers in #92 are accurate, then there's the question of whether what this offers us is worth that cost. I looked at what the library does and how the patch integrates it, and here's my 2 cents:
From what I can tell, this is true. Symfony's
Request::getAcceptableContentTypes()returns mime types in the order they appear in the header, not in quality order. And ourContentNegotiation::getContentType()just uses that order: it doesn't implement a sort by quality either. So, yeah, the StackNegotiation library is nice in that it implements http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html, but in practice, I don't know if there are user agents that we care about that don't order their accept header in order of preference to begin with, so it's possible the benefit is more theoretical than practical.Yes, but that's because our
ContentControllerSubscriber::onRequestDeriveFormat()runs at a fairly low priority. We can raise its priority, or even implement it as a Stack middleware without incorporating the StackNegotiation library.That bug now appears fixed, regardless of this patch, by virtue of HEAD's
AcceptHeaderMatcher. What's left related to that is that RestExport::initDisplay() has this code:Given that AcceptHeaderMatcher ensures that this code only runs if the agent accepts what the route can provide, the above code is not incorrect. But it's unclean. It would be nice if $request_content_type wasn't set to 'html' to begin with if the route doesn't return HTML. But the patch in this issue doesn't fix that. It potentially lays the groundwork for that fix, since the library's
getBest()method includes a $priorities parameter, so after routing, we could invoke it to find out the best content type that the browser accepts that the route can return. But potentially, we can do something similar even without the better library.In summary, I think the library is cool, and being RFC compliant is cool, but if we think the library is too slow and are willing to sacrifice RFC compliance in favor of performance, then I think we can still implement cleanups to code like the above even without the library.
Comment #95
Crell commentedAlex: Daniel and I discussed the possibility of doing a "current lib refactor" patch in the WSCCI meeting this morning; same as we're doing here, but with a middleware for the current crappy conneg lib rather than StackNegotiation, so at least getRequestFormat() becomes reliable. It should also have no performance impact as it's just moving existing code around. (If anything there might be a negligible improvement from not calling the lib multiple times.)
I still think we should do proper negotiation, but at least that way switching to it later would be zero API change instead of minimal edge-case change if we're still going to be stuck on performance measurements here.
Comment #96
chx commented> It should also have no performance impact as it's just moving existing code around
Do we negotiate currently if the page cache fires?
Edit: I seriously doubt. But I guess there's some ordering of middlewares we can exercise...?
Comment #97
chx commentedThis deserves a separate comment: page cache currently is a 200 priority middleware. For some reason this patch adds negotiation with a 400 priority. Can this patch be salvaged by changing the priority to 100?
Comment #98
dawehnerYeah I would not really trust those benchmarks (probably too much actual load on my local machine, which seems just matter on page caching as there is less work to do in general),
but at least the absolute number of function calls is deterministic.
Comment #99
dawehnerAfter a run debugging / discussion session with @fabianx, @larowlan, @chx we realized that the content-neg is required to run properly
for page caching, as #1855260-44: Page caching broken by accept header-based routing already figured out.
If you have
application/hal+jsonas accept header, you will retrive the HTML cache entries, because the hal subscriber, comes after page cache.This patch fixes the bug already.
Comment #100
znerol commentedI believe that #2019123: Use the same canonical URI paths as for HTML routes was an utterly bad idea. Not only the internal page cache is broken, but also every proxy cache. Unless perhaps if we start to add
Vary: Acceptfor routes which are also accessible via REST. Catch noted in #1855260-81: Page caching broken by accept header-based routing (and I very much agree):It seems that page caching was not discussed at all in #2019123 and I think we might want to reconsider this. Also note REST - Tradeoffs between content negotiation via Accept header versus extensions on SE.
Comment #101
fabianx commentedVary: Accept sounds great to me and the router knows if there is more than one route on the same path, so should be relatively "simple".
The REST thing is important to be REST-compliant.
Comment #102
znerol commentedREST does not mandate that the same paths are used as for the HTML content. I very much expect that existing implementations in other serious projects do not use the same paths for HTML and REST because of the implications for caching etc.
Comment #103
catchAkamai doesn't work with
Vary: Acceptsee #1855260-25: Page caching broken by accept header-based routing.Also chx linked to https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation and http://jamesrdf.blogspot.ca/2009/09/accept-headers-in-wild.html in irc.
Relying on browsers to send exactly the same accept header all the time is not going to result in good hit rates.
Thanks znerol for finding the two year old issue where this was already pointed out in depth, before #2019123: Use the same canonical URI paths as for HTML routes was even opened.
Comment #104
fabianx commentedI take back what I said about Accept: Vary and REST. I got more information now.
I still think that its better to leave things now for this issue and open / re-open another one for that kind of things.
Comment #105
daffie commentedMaybe we should wait with this patch until #2230121: Remove exit() from FormBuilder get solved. Because the have some difficulties with the issue and moshe weitzman has said (comment #39):
Comment #106
dawehnerNote: Once we have this code in place we could also just replace the stack implementation with an event subscriber which happens way before routing and be done.
Comment #107
fabianx commented#106 I don't see how an event is better here than the stack you can "subscribe" to ...
/me confused
Comment #108
Crell commentedWhat URLs something lives at is off topic for this thread. However, it's also not a binary "REST vs. HTML" question. We support, out of the box, at least 3 if not four different formats on entities. (HTML, JSON HAL, raw JSON, and I think XML HAL but I forget if we implemented that one.) Putting all of them at different URIs is not reasonable, so just splitting HTML and "not HTML" off to different URIs won't solve anything cache-related.
If anything, doing proper negotiation pre-cache lets us use the resolved accept format as a cache key rather than the full Accept header. That doesn't help with Varnish, true, but as above just splitting the URIs for HTML and REST wouldn't help with that either.
The question here is:
1) Is this architecturally better than what we have now. (IMO, unequivocally yes because now getRequestFormat() works reliably and we can clean up a lot of other code.)
2) Is using a more robust conneg library like StackNegotiation worth its performance impact (depends on what that impact is, which it sounds above like there's some question about).
The use of wrapped kernels itself is a red herring here.
Comment #109
znerol commentedFiled #2364011: [meta] External caches mix up response formats on URLs where content negotiation is in use.
Comment #110
fabianx commentedThis needs profiling, besides this, this is RTBC.
Per Dries this should be ready till tomorrow Oct 28th Amsterdam deadline.
So lets get this in ASAP.
Comment #111
fabianx commentedResult with Drupal 8 standard installed:
PHP 5.5 with opcache, front page, no content, just stock installation
TL;DR:
0.8% overhead, around 1 ms, 218 more function calls, 100 kB more memory usage, measured several times, margin of error is around 0.1 ms
Looks reasonable to me.
EDIT (this was done after Crell already re-RTBCed), results for page cache:
Again around 1ms (here even only 0.5 ms). Where 5% sound much, but its really just the difference between 10ms and 10.5 ms, which is perfectly okay ...
XHProf-Kit had a bug, that is why dawehener got the MAX instead of the minimum result ...
On that topic:
10.5 ms for a full page cache retrieval? Wow, that is really fast.
Comment #112
Crell commentedI think a 1 ms overhead for the cleanup here plus considerably more robust conneg is an acceptable cost. Thanks, Fabian!
Back to RTBC.
Comment #113
fabianx commentedI 100% agree with RTBC, it really is below 1 ms of overhead - I also tried different Accept formats.
The Format Negotiation library getBest() call itself showed as an overhead of 0.258 ms, which should be totally okay for the win.
Comment #114
chx commentedI have no opinion about the RTBC (really) but I really would not like to see another page cache affecting issue RTBC'd without any profiling or even considering the performance hit. I am absolutely shocked to see who everyone participated in this issue (even Dries!) and noone asked about performance. I think I will keep my avatar.
Comment #115
effulgentsia commentedIn #81, Dries did not raise objections to the library itself. Since this now comes down to a performance cost acceptability question, assigning to catch.
Comment #116
chx commentedThis issue really is a fantastic textbook example of everything wrong with Drupal 8. Not just that three core committers, two initiative leads and more subsystem maintainers than I want to count have skipped raising the performance question (even after webchick pointed out that Drupal is used on high traffic sites!!) but also there is only one comment here which "addresses" the version question and that is #73 which is eerily reminiscent of the handwaving in #1874530-2: Add Symfony CMF Routing component -- basically the answer is, version numbers are meaningless and... and that's it. I could list questions all day long: who did the review of the test coverage of this library ? Issue summary says "it's well-tested" -- well, thanks. Any coverage numbers? If it's as used as it is claimed, well, any blog posts, case studies etc -- there's absolutely nothing of the sorts. And altogether, why this particular library? Where is the research done to compare the available solutions?
This shouldn't stop the commit of this one because it's not worse than so many others we added just this happened to completely blew my fuse.
Comment #117
dawehnerRE: research ... updated the IS. Quite some research was done.
Comment #118
fabianx commentedI do agree it was pre-mature to RTBC this without taking the performance core gate into account. However this is a one-time thing and while in the critical path, not that relevant compared to things running more often. This at least was my reasoning not to ask for profiling of this - though that was wrong.
I do hope my benchmarks of both the page cache and non-page cached version help alleviate concerns around the performance impact of the library itself.
I do think we should ask upstream to release an official 1.0.0 version of the StackNegotation library, but I don't think this needs to block the commit of this - given its Amsterdam beta target.
To clarify on versions:
The actual negotiation library (https://github.com/willdurand/StackNegotiation) is "willdurand/negotiation": "~1.3", which is a very stable version.
It is _just_ the Stack integration that is tagged 0.1.0.
Both libraries have extensive tests - and I verified some of them.
Thanks!
Comment #119
fabianx commentedComment #120
fabianx commentedAdding myself to proposed commit message
Comment #121
willdurand commentedHi! My name is William and I am the "author" of both Negotiation and StackNegotiation.
Negotiation is stable and runs in production for a while now, it has been installed +700k times, and is a class-first citizen in FOSRestBundle, THE REST toolbox for Symfony applications. The API will not change, but internal refactoring may happen to support even more edge cases. Note that this library does not aim at "patching/hidding" incorrect behaviors caused by silly browsers (I am looking at you, IE8).
StackNegotiation works well too, however I never used it in production. That's why I never released a stable version. However, I don't plan to change the API, it works well after all and the integration is rather simple. I can release a stable version and maintain it if you want to rely on stable packages (which makes sense). Let me know.
Also, if you would like to make changes, let's discuss them *before* the release of a stable version :-)
Regards,
William
Comment #122
dawehner@william
There is this browser crazyness but sure, we can ship it in Drupal directly.
Comment #123
Crell commentedThanks, willdurand. If there's no API changes planned for the foreseeable future then having a stable tag to reference in our composer.json would be appreciated. We're used to dealing with "pseudo-stable" modules in Drupal (a LOT of major modules do that) but it's a practice we try to discourage for Drupal developers, so having a library we depend on using a stable tag would help make that case. :-)
Since we're specifying a tag already here I don't think that's a commit blocker; it's easy enough to switch later if needs be. I am still +1 on commit here.
Comment #124
willdurand commentedI didn't follow the whole discussion but it is worth mentioning that this middleware does not provide any way to configure fallbacks/priorities yet, which could be interesting. WDYT? Also, it requires at least PHP 5.4.
Comment #125
willdurand commentedPriorities are now supported. As for "fallback", it should probably not be part of this middleware.
Comment #126
fabianx commentedDrupal is >= PHP 5.4 due to dependencies, trait usage and [] syntax and PHP 5.3 is EOL, so that should be fine.
In which cases would we need to use "fallbacks"?
I think if we can plan a stable release of StackNegotiation once we have this in, that would be appreciated.
Comment #127
willdurand commentedFallback may be useful if it is not possible to find a format based on the accept header data, but this can be handled somewhere else.
Comment #128
catch@Crell surely
Isn't that the entire topic of this thread when it comes down to it? I'm really fed up with these blanket, contentless statements that attempt to shut down valid discussion.
I brought up the pre-page-cache negotiation requirement as a performance issue, as well as the additional complexity for external caches on #1855260: Page caching broken by accept header-based routing based on the original WSCCI plans, months before those features were implemented. So it is extremely disappointing to be reading this issue discuss all of that over again nearly two years later, without any of that original discussion ever having been "on topic" while this was implemented, and still with no viable plan on how to resolve the issues for external page caches.
Vary: Acceptis not viable as shown here and in previous discussions.Putting the different response formats at different URIs would be fine with me, there's not really an argument against it except it not matching REST - but examples of genuine RESTful implementations that successfully resolve these issues have been completely lacking throughout this process.
Comment #129
Crell commentedNo, catch, it's not. We could eliminate the need for conneg entirely if and only if we completely and utterly ignore the Accept* and Content Type headers in all requests and force every single different format of every resource to have its own distinct URI. That is, not follow HTTP, revert back to D7 logic and remove the word "REST" from any and all of our self-description (because to do otherwise would frankly be lying).
This issue is about, solely, improving the conneg we have (we already have a conneg library and have for around 2.5 years) and fixing the API around it (making $request->getRequestFormat() reliable, which right now it is not). Ripping out the concept of content negotiation entirely (which is what the discussion of moving every resource to its own URI is) is not on topic for this issue. At all.
Comment #130
catchI've bumped #2364011: [meta] External caches mix up response formats on URLs where content negotiation is in use to critical, answering that should really should have been a pre-requisite to #2019123: Use the same canonical URI paths as for HTML routes happening rather than punted down the line against objections at the time.
Comment #131
effulgentsia commentedSo long as HEAD performs format negotiation based on an Accept header, I'm reclassifying this as a bug, because:
- Not parsing the Accept header to the RFC spec is a bug
- Having
$request->getRequestFormat()return the wrong answer based on when it's invoked is a bugPer #2350615: [policy, no patch] What changes can be accepted during the Drupal 8 beta phase?, normal priority bug fixes are still allowed during the beta phase, so long as their value exceeds their cost.
Since there's discussion on #2364011: [meta] External caches mix up response formats on URLs where content negotiation is in use about potentially removing header-based format negotiation entirely, I think we should postpone this on a resolution of that discussion, but I'm leaving that decision to catch.
Comment #133
Crell commentedAt absolute minimum we should do as #95 suggests and just move the code around. That's a straight up "take an inconsistent API that makes little sense and make it consistent and make sense" task, which should be uncontroversial, even if it has no change in the quality of the accept negotiation itself.
Even if we were to rip out Accept header negotiation entirely (which I think would be a huge mistake), the fallback would still use the _format key on the request to track the format in question (even if that value comes from the URI), and so we need getRequestFormat() to be reliable and predictable.
Comment #134
chx commentedHow much human effort would it be to replace the current broken conneg with willdurand/Negotation ? If not a lot, why don't we do that to get a non-broken negotation that barely affects performance as it fires after the page cache. Then, as things resolve, perhaps add stackneg or perhaps not.
Comment #135
effulgentsia commentedRe #134:
Per above, conneg is already happening before page cache in HEAD, so can't remove that so long as we're serving different representations at the same URL, so no way to switch to a slower implementation without taking the hit.
Comment #136
dawehnerWe can work moving the current implementation into a middleware. This allows us to fix bugs. We can then decide later whether its okay to accept the performance, especially because you can swap it out easily in contrib.
Comment #137
catch#136 sounds like a good plan.
Comment #138
fabianx commented#136: Can we do that as another issue, please however, to leave this one to wildurand/negotiation / StackNeg?
Comment #139
willdurand commentedWhat are the performance issues you're talking about?
Comment #140
fabianx commentedThere a no real "performance issues", because as the above benchmarks show the overhead of the library and moving to stack is 0.5 - 1 ms. That is why we want to move to stack first, then exchange the library. That will allow us to judge the overhead better.
The path to the page-cache (similar to symfony HttpCache), is critical however and takes 10 ms currently. It took 2 ms in the previous Drupal 7 version and the reason to optimize is not only raw speed, but also how many clients you can serve from that database cache (e.g. raw computing overhead determines scalability).
However going that overall route we need to send Vary: Accept per rest specification (which is bad for reverse proxy cacheability), so we discuss various options in other issues of what we need to be "fast-by-default".
Comment #141
dawehner@fabianx
Sure, here is the other issue #2369681: Move ContentNegotiation into a middleware
Comment #142
Crell commentedI guess this is postponed on #2369681: Move ContentNegotiation into a middleware now...
Comment #143
Crell commentedPer discussion in #2364011: [meta] External caches mix up response formats on URLs where content negotiation is in use, we won't be going this route. Sad panda.