Problem/Motivation

  • Drupal\Core\ContentNegotiation has a @todo saying "Replace this class with a real content negotiation library".
  • #1505080: [META] Content Negotiation for Accept Headers is the issue for doing that, though it's not clear whether that can still happen in time for core or not.
  • In any case, the API of that class is fundamentally flawed, so should be changed (ideally, before the July 1 API freeze), so that we can either implement a real content negotiation library in core after API freeze, or do it in contrib.
  • The flaw is that ContentNegotiation::getContentType($request) is expected to return a string: meaning, a known format. However, which format is appropriate is a question whose answer depends on routing information. For example, if the Accept header is text/html, */* and there's a route for the requested URL that can return HTML, then the answer is 'html', but if there's only a route for the requested URL that can return json, then the answer is 'json'. And if there are multiple routes for the requested URL: one that can return json and one that can return xml, then the routing system may want to have a say as to which one is higher priority (the Accept header may also include priority information, but in the example above, it doesn't). The current implementation of ContentNegotiation checks only against formats statically defined as potentially available, but takes no consideration of routing information to determine what's available for the requested URL.
  • We currently have a hack in RestExport::initDisplay() to get around this problem for its own use case: it ignores ContentNegotiation::getContentType() if it returns 'html', since it knows that its entire purpose is to return something other than 'html'. However, a controller needing to overrule the content negotiation service is an indication of the content negotiation service being broken.
  • On a DX note, getContentType() is a poor method name, because the request object also has a getContentType() method, but that one returns the content type of the request body, which has nothing to do with the desired format of the response.

Proposed resolution

  • Symfony's Request class already has a getRequestFormat() method whose job is to return the single desired format of the response, as determined by routing or whatever else can act on a $request. Everywhere in Drupal that we currently call $this->negotiation->getContentType($request) should be changed to call $request->getRequestFormat() instead. At this time, that's:
    - AjaxEnhancer::enhance()
    - ContentControllerEnhancer::enhance()
    - EntityRouteEnhancer::enhance()
    - FormEnhancer::enhance()
    - drupal_get_js()
    - ViewSubscriber::onView()
    - OverlaySubscriber::onRequest()
    - ExceptionController::execute()
    - RestExport::initDisplay().
  • The only thing that should interact with the content negotiation service directly is the routing system itself. We already have a 'mime_type_matcher' service that filters out routes whose format requirement is incompatible with the Accept header. What we need to add is to then somehow let the content negotiation service pick the best match, or provide information to something in the routing system that can then pick the best match. And then based on that, the routing system needs to either set the '_format' attribute of the $request, or call $request->setRequestFormat() (I'm not too clear on the pros/cons of whether to do the former, latter, or both).

Remaining tasks

  • Figure out the actual API we want ContentNegotiation to have. People involved in #1505080: [META] Content Negotiation for Accept Headers, who know how those libraries work, might have input on this.
  • Add something into the routing flow that uses this API to do what's above in "Proposed resolution".

User interface changes

None.

API changes

See above.

#1505080: [META] Content Negotiation for Accept Headers

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

effulgentsia’s picture

Issue summary: View changes

Updated issue summary.

Crell’s picture

I believe the code need is:

1) The mime matcher needs to filter and reorder routes, so that the first-matchable one is what will get picked up by UrlMatcher.
2) We need to ensure we always set a _format attribute on a route that's been matched, so downstream can leverage that.

Crell’s picture

Issue summary: View changes

Updated issue summary.

Crell’s picture

Assigned: Unassigned » Crell
Issue summary: View changes

This fell off my radar, but I'm going to try and put it back on.

I'm not sure if it qualifies as an API change per se. It kind of does, but only for systems most modules won't be touching.

Crell’s picture

Status: Active » Needs review
FileSize
11.66 KB

OK, I decided to make a new RouteEnhancer rather than using MimetypeMatcher, since the latter has multiple other patches messing with it.

This will also conflict with the HtmlPage issue. I hate rerolling patches. :-(

Crell’s picture

Ugh, I did add a related issue. The issue queue is barely usable at this point. :-(

dawehner’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +PHPUnit
FileSize
14.19 KB

Wonderful!!

Let's just add a unit test.

Crell’s picture

Status: Reviewed & tested by the community » Needs review

I am going to un-RTBC this, if only because it will conflict with the HtmlPage issue and that is about 400 times harder to reroll. I'd MUCH rather reroll this issue once that's in, and we're actively working on it now. So, please sit tight, this will be back soon (I hope).

Crell’s picture

Crell’s picture

FileSize
10.95 KB

Rerolling now that #2068471: Normalize Controller/View-listener behavior with a Page object is in...

Patch is smaller because half of the work was done already in that issue through related refactoring.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC

catch’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/lib/Drupal/Core/Routing/Enhancer/RequestFormatEnhancer.php
@@ -12,9 +12,12 @@
+ *
+ * In this case we are modifying the request, not the route data, but it
+ * is the most logical place to set this value.
  */

Why is it the most logical place?

Crell’s picture

Status: Needs review » Reviewed & tested by the community

Because there is no other place to do so. It needs to happen after matching (because we don't know what route we have until then) but before the other enhancers fire (because they rely on knowing the mime type to determine the wrapping controller). There is no operation between matching and enhancing other than a high-priority enhancer.

It's not my preferred place to do so, it's just the only one I know of.

catch’s picture

Status: Reviewed & tested by the community » Needs work

OK I can live with it if it's the only place, but the information in #11 should go in a comment.

dawehner’s picture

Status: Needs work » Needs review
FileSize
10.8 KB
1008 bytes

Let's just do it.

Crell’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

Once again, dawehner saves my day. :-)

catch’s picture

Status: Reviewed & tested by the community » Needs work

Sorry the more I look at this the less right it looks. Relying on listener priority for fundamental stuff like this feels very fragile indeed. I don't have much suggestion on how to fix though, but a new event in between the two maybe? And even with that comment I still don't know understand we're altering the request in a route enhancer.

alexpott’s picture

In irc I expressed similar reservation as @catch - a router enhancer not enhancing routes just does not make much sense. And also if this is the solution going with a priority of 100 doesn't make much sense either. Maybe 254 or 255 as 255 is the theoretical maximum. But even just considering this shows that the solution is wrong in my opinion.

dawehner’s picture

....

dawehner’s picture

Opened a pull request to have a proper event in place: https://github.com/symfony-cmf/Routing/pull/93/files

Crell’s picture

We discussed this at length at last week's meeting. What we're going to do instead is take the code as written here, but move the new conneg enhancer and the existing _controller-adding enhancers to their own request listeners that happen post-matching. Order control will still be via priority, but it will be a little less weird than using the route enhancer to modify the request.

Key participants in the discussion: dawehner, msonnabaum, Crell, and lsmith.

Crell’s picture

FileSize
47.36 KB
9.54 KB

Following on from #1959574: Remove the deprecated Drupal 7 Ajax API, this does what was described in the previous comment. I did not touch the AjaxEnhancer since there's still an open question of whether or not we'd even keep it, and the Entity enhancer is, I think, not relevant to this discussion.

Since the noted patch isn't in yet, this is rolled on top of it. Look at the for-review version for what I've changed here.

Crell’s picture

Status: Needs work » Needs review

Grumble grumble issue queue...

dawehner’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
YesCT’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
9.54 KB

I started to reroll the first patch from #20, then when I had a question in irc, @dawehner pointed out that the for review one was just the changes needed since #1959574: Remove the deprecated Drupal 7 Ajax API got in. That applies to head, and passes phpunit tests, so reuploading it with a different name so it gets tested.

this is the same as @Crell's 2026431-accept-format-for-review-do-not-test.patch

Status: Needs review » Needs work

The last submitted patch, 23: 2026431-accept-format-from-20b.patch, failed testing.

YesCT’s picture

Status: Needs work » Needs review
FileSize
9.58 KB
1.21 KB
0 bytes

first interdiff is a fix for the php error. (phpstorm said that would have worked in php 5.5)
does the fact that the phpunit tests didn't fail mean that there is missing test coverage of that?

second interdiff is nits.

YesCT’s picture

actual nit interdiff this time.

Status: Needs review » Needs work

The last submitted patch, 25: 2026431-accept-format-25.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
10.05 KB
482 bytes

@YesCT Thanks for the rerole!

This could be it.

Crell’s picture

Status: Needs review » Reviewed & tested by the community
Related issues: +#2019123: Use the same canonical URI paths as for HTML routes

Thanks, folks. Although this will conflict with #2019123: Use the same canonical URI paths as for HTML routes :-( (That's why I'd been holding off on this one.)

dawehner’s picture

Come on, this is such a small patch compared to the other one.

Crell’s picture

Yes, I didn't want to break the other one, either. :-)

dawehner’s picture

We could also just mark this one as blocking for the other one and so push it to critial :P

webchick’s picture

Assigned: Crell » catch

Looks like catch has had his paw prints on this one.

catch’s picture

I've been following this, and the idea is fine, I think I'm having trouble getting over my intense dislike of the API for event subscribers, but that's not very useful in the context of this particular issue I guess.

One question though:

  1. +++ b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
    @@ -0,0 +1,93 @@
    +    $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormat', 31);
    

    31

  2. +++ b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php
    @@ -0,0 +1,93 @@
    +    $events[KernelEvents::REQUEST][] = array('onRequestDeriveContentWrapper', 30);
    

    30

  3. +++ b/core/lib/Drupal/Core/EventSubscriber/ContentFormControllerSubscriber.php
    @@ -57,14 +58,29 @@ public function __construct(ContainerInterface $container, ControllerResolverInt
    +    $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormWrapper', 29);
    

    29

Is there really no need to leave any space between these?

Crell’s picture

Those numbers were chosen because 32 is the priority of the routing itself (selected because that's what Symfony fullstack does; why I have no idea) and we wanted those to run as soon as possible afterward, in a specific order. I cannot think of a use case for injecting something between them; certainly another listener could use the same priority if it doesn't care which runs first, as long as it's before another in the list. Access control is currently 30 (AccessSubscriber), and I felt we wanted the connect to happen first.

If we want to rejigger the priorities of various other listeners to "make room" I'm open to that, but IMO that should be a follow up. Once this is in I want to investigate the alternate library from #1505080-52: [META] Content Negotiation for Accept Headers, which is what this centralization allows us to do.

dawehner’s picture

One major reason why these numbers are like that is that 32 is the routing. We want to update the _content and _controller bit as early as possible after routing, so other subscribers get the actual proper information.
One usecase you might think of could be to derive a different request format. I kinda think we should have an interface for the content negotiation so people could just switch out that service in contrib.

Btw. there is nearly infinite space between two ints as nothing blocks you from using floats.

YesCT’s picture

28: content_negotation-2026431-28.patch queued for re-testing.

[edit: sorry! forgot about #1952058: [META] Retesting stale RTBC core patches manual retests like this wont be needed ]

catch’s picture

Status: Reviewed & tested by the community » Fixed

Alright I guess the floats are there if you really need it, it's not wonderful but it's not completely locking out.

Like the interface idea - could we open a follow-up for that?

Committed/pushed to 8.x, thanks!

  • Commit 99b9a6a on 8.x by catch:
    Issue #2026431 by YesCT, dawehner, Crell: Make ContentNegotiation a '...
Crell’s picture

The next step IMO is to try swapping our crappy library out for a real one: #1505080-52: [META] Content Negotiation for Accept Headers

Status: Fixed » Closed (fixed)

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