Problem/Motivation

#2368987: Move internal page caching to a module to avoid relying on config get on runtime moved the page caching to its own module and removed the caching setting instead (so caching is dependent on the module being enabled or not). Before that, the browser detection only worked if page caching was turned off. That setting was removed, so browser language negotiation now runs even with page caching enabled, but is not cached. It uses the kill switch instead:

\Drupal::service('page_cache_kill_switch')->trigger();

To make it cacheable, we need to overcome a few problems.

A. If browser language negotiation is before URL language negotiation, you may get different language negotiated for the same path. We should ensure browser negotiation is after URL negotiation.
B. Once we ensure that browser negotiation is after URL language detection, we need to ensure that browser negotiation redirects to the correct language specific URL.
C. You may use browser negotiation without URL language detection. We need to somehow ensure that you only use browser language negotiation if we can redirect to a concrete URL that is explicit for the negotiated language (contrib negotiation methods may be involved, session negotiation may be fed with an URL parameter to set this, etc).
D. Even if you use browser negotiation and URL negotiation, if one of the languages has an empty path prefix, it will fall through URL detection, so may still have different languages or is not redirectable to. Needs to be ensured that if browser language negotiation and URL negotiation is used, all languages have a prefix.

Proposed resolution

Problem B and C (implemented in #65)

Implement language redirect:
- Implement a new method onKernelRequestLanguageRedirect() on the LanguageRequestSubscriber.
- This method is called after current language is detected and routes are initialized.
- The method first ensures that redirect is allowed: request method is GET, there is no "destination" parameter in the current request, etc.
- The method constructs URL to the current route. The URL is processed with URL outbound processors, so it may contain language path prefix or other language identifier.
- The method compares the result URL with the requested URL. If the differs, it assumes that the language identifier was added and redirects to the result URL.

Here is a behavior example for the case when a) URL negotiation is enabled and language prefixes are set up properly, b) browser negotiation is enabled and goes after the URL negotiation.

Before patch:
- "domain.com/" URL accessed
- "domain.com/" page cache is killed
- all links rendered on the page leads to language prefixed URLs

After patch:
- "domain.com/" URL accessed
- "domain.com/" page cache is killed
- user is redirected to language prefixed URL ("domain.com/en") which can be cached

So, the patch does not avoid cache kill, instead it introduces language redirect. (Which in D7 was usually performed by globalredirect module.)

Problem A and D

Implement hook_requirements() that warns user about the possible cache issues
- if there is more that 1 language and the path prefix is not set for some language
- if browser negotiation is the only enabled method
- if URL negotiation is set after the browser negotiation

Alternatively, the language_negotiation_url_prefixes_update() function can be updated to always add language path prefix to all languages. But this requires some tests update, see #66 for details.

Alternative resolution

(implemented in #116)

- Introduce new response policy `Vary`, that will be less disruptive than `KillSwitch`.
- Modify PageCache to respect Vary headers coming from response.

Latter is a good thing in general, going beyond the scope of language detection.
Currently PageCache distinguishes cached pages only by predefined "tags": host, URL, and requested format. The only supported Vary header is cookie, and it's handled in ad-hoc manner.

Since page cache occurs on the early stage of request processing, the fastest/easiest way to get the list of vary headers is to fetch them from "generally" cached response object, and then check if there are any headers in request object that match vary criteria.

Regarding redirect and related problems A-D, I believe they should be handled in #2641118: Route normalizer: Global Redirect in core, and this issue should take care of cache awareness of browser language detection.

Remaining tasks

+ Fix tests
+ Write specific tests
- Review
- Maybe address Problem A and D.

User interface changes

Possibly errors for unsupported configurations.

API changes

Not likely. Additions needed.

CommentFileSizeAuthor
#194 2430335-194.patch11.14 KBsmustgrave
#194 interdiff-193-194.txt1.09 KBsmustgrave
#193 2430335-193-10.1.patch11.17 KBaludescher
#190 2430335-190.patch10.67 KBsmustgrave
#190 interdiff-185-190.txt1.2 KBsmustgrave
#185 2430335-183-8.9.patch10.62 KBdorficus
#182 2430335-reroll-181-8.9.patch10.64 KBdorficus
#179 reroll_diff_173-179.txt12.96 KBdorficus
#179 2430335-reroll-179-8.9.patch5.46 KBdorficus
#173 2430335-173-8.9.patch11 KBLukas von Blarer
#170 2430335-165-9.1.patch-issue.jpg16.81 KBshobhit_juyal
#167 Screenshot 2020-05-01 at 10.33.34.png8.02 KBFalco010
#165 2430335-165-9.1.patch11.12 KBandypost
#165 interdiff.txt2.66 KBandypost
#164 2430335-164-149-9.1.patch10.75 KBandypost
#160 Screen Shot 2019-07-25 at 10.20.31.png104.59 KBLukas von Blarer
#155 detection_language-2430335-155.patch0 bytesclairedesbois@gmail.com
#149 2430335-149-8.7.x.patch10.75 KBstBorchert
#133 2430335-133-complete.patch15.66 KBjofitz
#133 2430335-133-test_only.patch15.3 KBjofitz
#133 interdiff-2430335-124-133.txt933 bytesjofitz
#124 2430335-123-testonly.patch15.32 KBvalthebald
#124 interdiff-2430335-120-123.txt8.71 KBvalthebald
#124 interdiff-2430335-123-fix.txt1.02 KBvalthebald
#123 2430335-123.patch15.68 KBvalthebald
#120 2430335-120.patch10.1 KBvalthebald
#120 2430335-120-testonly.patch9 KBvalthebald
#120 interdiff-2430335-120-fix.txt942 bytesvalthebald
#120 interdiff-2430335-116-120.txt1.29 KBvalthebald
#116 2430335-116.patch10.11 KBvalthebald
#116 interdiff-2430335-112-116.txt2.84 KBvalthebald
#115 LanguageBrowserDetectionEmptyTest.txt4.26 KBsleitner
#115 2430335-115.inter-diff.diff5.14 KBsleitner
#115 2430335-115.patch18.25 KBsleitner
#113 2430335-112.patch9.78 KBvalthebald
#111 2430335-111.patch2.78 KBvalthebald
#89 2430335-89-perform_redirect_in_sysyem_module.patch3.74 KBLeksat
#87 2430335-interfidd-65-87.txt9.38 KBLeksat
#87 2430335-interfidd-86-87.txt3.72 KBLeksat
#87 2430335-87.patch14.56 KBLeksat
#86 interdiff-82-86.patch.txt1008 byteskarmazzin
#86 2430335-86.patch14 KBkarmazzin
#82 interdiff-65-82.patch6.95 KBkarmazzin
#82 2430335-82.patch13.02 KBkarmazzin
#71 interdiff-58-65.txt8.63 KBvictor-shelepen
#65 2430335-63-65-interdiff.txt4.92 KBLeksat
#65 2430335-65.patch12.31 KBLeksat
#63 2430335-61-63-interdiff.txt1.56 KBLeksat
#63 2430335-63.patch11.71 KBLeksat
#61 2430335-61.patch6.89 KBLeksat
#58 2430335-browser_language-58.patch9.75 KBvictor-shelepen
#56 2430335-another_approach-56-do-not-test.txt3.33 KBLeksat
#54 2430335-interdiff-49-54.txt12.12 KBLeksat
#54 2430335-54.patch17.7 KBLeksat
#49 interdiff-37-49.txt4.32 KBvictor-shelepen
#49 2430335-language-negotiation-redirect-49.patch18.05 KBvictor-shelepen
#37 2430335-interdiff-33-37.txt1.72 KBLeksat
#37 2430335-language-negotiation-redirect-37.patch13.58 KBLeksat
#33 interdiff-31-33.txt2.53 KBSchnitzel
#33 24303352430335-language-negotiation-redirect-33.patch12.54 KBSchnitzel
#31 interdiff-29-31.txt1004 bytesSchnitzel
#31 24303352430335-language-negotiation-redirect-31.patch12.27 KBSchnitzel
#29 interdiff-27-29.txt1.47 KBSchnitzel
#29 24303352430335-language-negotiation-redirect-29.patch13.25 KBSchnitzel
#27 interdiff-25-27.txt1.58 KBSchnitzel
#27 24303352430335-language-negotiation-redirect-27.patch13.44 KBSchnitzel
#25 interdiff-20-25.txt1.08 KBSchnitzel
#25 24303352430335-language-negotiation-redirect-25.patch13.17 KBSchnitzel
#20 interdiff-18-20.txt969 bytesSchnitzel
#20 2430335-language-negotiation-redirect-20.patch13.96 KBSchnitzel
#18 2430335-language-negotiation-redirect-18.patch13.81 KBSchnitzel
#18 interdiff-14-18.txt13.81 KBSchnitzel
#14 interdiff-12-14.txt1.08 KBSchnitzel
#14 24303352430335-language-negotiation-redirect-14.patch12.83 KBSchnitzel
#12 interdiff-10-12.txt11.55 KBSchnitzel
#12 24303352430335-language-negotiation-redirect-12.patch12.04 KBSchnitzel
#10 24303352430335-language-negotiation-redirect-10.patch8.93 KBSchnitzel
#7 2430335-browser_language-7--2368987-page_cache_module-124.txt4.61 KBvictor-shelepen
#7 2430335-browser_language-7.patch108.66 KBvictor-shelepen

Issue fork drupal-2430335

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Issue tags: +D8 cacheability
Gábor Hojtsy’s picture

Also based on \Drupal\Core\StackMiddleware\PageCache::lookup() we should set a 'no-cache' directive when setting a redirect, so its not cached.

catch’s picture

Fabianx’s picture

The proposal from an IRC meeting with Wim, catch and berdir was:

- Whenever browser or url language or user language negotiation is in play, send a 302 and a Cache-Control: no-cache header. (possibly using the PageCache request policy or a cache_context.uncacheable)
- On language aware paths (/en,/fr,/xyz) the path always takes precedence and this can be cached then.
- If a user language is negotiated cache_context.user needs to be set.

Gábor Hojtsy’s picture

Issue tags: -sprint

It does not seem like anyone is working on this one :/

Fabianx’s picture

victor-shelepen’s picture

There is a proposition to modify cid: we need get the real page path, add a language key a separated key.

It returns the bare path.
$uri = $this->pathProcessor->processInbound($uri, $request);

It returns the current language according to Negotiators.
$language = $this->languageManager->getCurrentLanguage();

So the page cache becomes more flexible.

protected function getCacheId(Request $request) {
    $uri = $request->getRequestUri();
    $language = $this->languageManager->getCurrentLanguage();
    $langcode = $language->getId();
    $uri = $this->pathProcessor->processInbound($uri, $request);
    $langcodes = array_keys($this->languageManager->getLanguages());
    if($this->languageManager->isMultilingual()) {
      $parts = explode('/', $uri);
      if (in_array($parts[1], $langcodes)) {
        array_shift($parts);
        $langcode = array_shift($parts);
        $uri = implode('/', $parts);
      }
    }
    $cid_parts = array(
      $uri,
      $langcode,
      $request->getRequestFormat(),
    );

    return implode(':', $cid_parts);
  }

Status: Needs review » Needs work

The last submitted patch, 7: 2430335-browser_language-7.patch, failed testing.

victor-shelepen’s picture

In my case this logic does not work. I do not know exactly why?

/var/www/d8/drupal/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php

// Test resolving the French homepage using the incorrect processor order.
$test_path = 'fr';
$request = Request::create($test_path);
$processed = $processor_manager->processInbound($test_path, $request);
$this->assertEquals('', $processed, 'Processing in the incorrect order fails to resolve the system path from the empty path');

// Test resolving an existing alias using the incorrect processor order.
$test_path = 'fr/foo';
$request = Request::create($test_path);
$processed = $processor_manager->processInbound($test_path, $request);
$this->assertEquals('foo', $processed, 'Processing in the incorrect order fails to resolve the system path from an alias');
Schnitzel’s picture

Assigned: Unassigned » Schnitzel
Status: Needs work » Needs review
FileSize
8.93 KB

here my first approach, it does:
- Introduce a new method reactSelectedLanguage() for language negotiation plugins to react to the selected language by another plugin
- LanguageNegotiationUrl plugin implements this method, generates the current request with language pathprefix or domain (via processOutbound()) and returns a RedirectResponse Object with a redirect tho this new url. Also forces Drupal to not cache this Redirect.
- Adapted LanguageNegotiator to be able to handle Response Objects <-- super super ugly, probably need another system to handle that, but currently it works.

missing:
- specific tests for this new method
- "If a user language is negotiated cache_context.user needs to be set" (by @fabianx)

not clear:
by @fabianx

"On language aware paths (/en,/fr,/xyz) the path always takes precedence and this can be cached then."

Do you mean we should change the order of the language negotiation plugins?

Let's see what the testbot says.

Status: Needs review » Needs work

The last submitted patch, 10: 24303352430335-language-negotiation-redirect-10.patch, failed testing.

Schnitzel’s picture

Status: Needs work » Needs review
FileSize
12.04 KB
11.55 KB

okay, new approach, the idea to pass the response through getCurrentLanguage() was a really bad idea, it is used so widely, it broke a lot of things.
The new approach has a new kernel.request subscriber that calls the negotiation plugins and if one of them returns a RedirectResponse object, this one is executed.

also found a bug that was checking the generated new URI against getBaseUrl() instead of getRequestUri, which then redirected way too much :)

still missing:
- specific tests for the whole thing.

Status: Needs review » Needs work

The last submitted patch, 12: 24303352430335-language-negotiation-redirect-12.patch, failed testing.

Schnitzel’s picture

Status: Needs work » Needs review
FileSize
12.83 KB
1.08 KB

new try, I think the issue actually is a Core Bug, because if we create a new language with ConfigurableLanguage::createFromLangcode() and after that directly call Url::fromRoute(), this URL does not have a path prefix... which it should.

The patch fixes that, but I think this should actually be it's own issue with a test, but let's see what the testbot says.

Status: Needs review » Needs work

The last submitted patch, 14: 24303352430335-language-negotiation-redirect-14.patch, failed testing.

znerol’s picture

@Schnitzel: Can you reproduce those test failures on your local machine? Could it be that they are due to differences between the UI and CLI test-runner?

victor-shelepen’s picture

@Schnitzel: Hello. Could you describe your idea? Sorry. But it is not clear for me. Thank you.

Schnitzel’s picture

Status: Needs work » Needs review
FileSize
13.81 KB
13.81 KB

looks like most of the errors are based because the tests are enabling languages and then make requests to URLs, but as we have now redirects we should follow them.

Status: Needs review » Needs work

The last submitted patch, 18: 2430335-language-negotiation-redirect-18.patch, failed testing.

Schnitzel’s picture

Status: Needs work » Needs review
FileSize
13.96 KB
969 bytes

some of the failing tests where failing because by default Drupal can have an empty path prefix for the default language (which I don't think is a good practice), but anyway, now not doing any redirects if one of the languages has an empty path prefix

Status: Needs review » Needs work

The last submitted patch, 20: 2430335-language-negotiation-redirect-20.patch, failed testing.

Gábor Hojtsy’s picture

Please update the issue summary with the proposed solution, it is not evident from skimming the patch or I had too little time, sorry. Would love to provide feedback on the architecture first before looking at how things are named for example :)

Gábor Hojtsy’s picture

Issue tags: +sprint
Schnitzel’s picture

Issue summary: View changes
Schnitzel’s picture

Status: Needs work » Needs review
FileSize
13.17 KB
1.08 KB

okay, that is strange, my fix for the problems in #14 actually created more issues. removed that again and now let's see.

Status: Needs review » Needs work

The last submitted patch, 25: 24303352430335-language-negotiation-redirect-25.patch, failed testing.

Schnitzel’s picture

Status: Needs work » Needs review
FileSize
13.44 KB
1.58 KB

looks like there are some issues during POST and redirects.
so only redirecting on GET.

Status: Needs review » Needs work

The last submitted patch, 27: 24303352430335-language-negotiation-redirect-27.patch, failed testing.

Schnitzel’s picture

Status: Needs work » Needs review
FileSize
13.25 KB
1.47 KB

whoops, actually need to call the method correctly ;)

Status: Needs review » Needs work

The last submitted patch, 29: 24303352430335-language-negotiation-redirect-29.patch, failed testing.

Schnitzel’s picture

Status: Needs work » Needs review
FileSize
12.27 KB
1004 bytes

Status: Needs review » Needs work

The last submitted patch, 31: 24303352430335-language-negotiation-redirect-31.patch, failed testing.

Schnitzel’s picture

Status: Needs work » Needs review
FileSize
12.54 KB
2.53 KB

I've sent the redirect header with an relative URL, which generated the issue that the webtest internal redirect follow handler couldn't load the redirect, even though this is RFC compatible (http://stackoverflow.com/a/25643550)
Now sending an absolute URL and should work much better.

Status: Needs review » Needs work

The last submitted patch, 33: 24303352430335-language-negotiation-redirect-33.patch, failed testing.

Berdir’s picture

FYI: There's an existing issue about this not working with page caching enabled in 7.x here: #2257843: Browser language detection is skipped for anonymous users when page caching is turned on, with no warning to the site administrator. I think we can backport the fix from #2368987: Move internal page caching to a module to avoid relying on config get on runtime to 7.x as well, please review :)

Gábor Hojtsy’s picture

Issue summary: View changes
Leksat’s picture

Status: Needs work » Needs review
FileSize
13.58 KB
1.72 KB

I've fixed the test fails. At least the "language" group tests pass on my local.

Changes:

1. I avoided redirect if the "destination" parameter exists in the request (diff). Some tests failed because drupalLogout() sent GET request to /user/logout?destination=user/login, the LanguageNegotiationUrl tried to redirect this to /en/user/logout?destination=user/login, then RedirectResponseSubscriber::checkRedirectUrl() retargeted this to /user/login (picked up from the destination parameter). The final URL was the user page, and of course user was not logged out.

2. I fixed the LanguageListTest fail by adding a resetAll() call (diff). The URL check failed because the Drupal container (which was used inside the test) was not updated with new language configuration. It contained information about the only registered language, so the LanguageServiceProvider::isMultilingual() returned FALSE, and did not register itself for the path outbound processing. And so the URL constructed inside test was different from the actual URL.

Fabianx’s picture

Overall looks great to me.

Wim Leers’s picture

Status: Needs review » Needs work

Hurray, thanks so much for pushing this forward :) This is a hairy issue, and solving it will help many thousands of sites!


A. If browser language negotiation is before URL language negotiation, you may get different language negotiated for the same path. We should ensure browser negotiation is after URL negotiation.
B. Once we ensure that browser negotiation is after URL language detection, we need to ensure that browser negotiation redirects to the correct language specific URL.
C. You may use browser negotiation without URL language detection. We need to somehow ensure that you only use browser language negotiation if we can redirect to a concrete URL that is explicit for the negotiated language (contrib negotiation methods may be involved, session negotiation may be fed with an URL parameter to set this, etc).
D. Even if you use browser negotiation and URL negotiation, if one of the languages has an empty path prefix, it will fall through URL detection, so may still have different languages or is not redirectable to. Needs to be ensured that if browser language negotiation and URL negotiation is used, all languages have a prefix.

How does this patch ensure A, C and D?

It is clear that B is addressed.


However, I think the patch contains a lot of small problems: it seems many things only happen to work in a specific set-up. More test coverage would be welcome to prove that this is robust. Method names are also quite confusing.

Overall, looks like this needs some polish before it can be committed. It's getting there, but it's not close to RTBC yet.


  1. +++ b/core/lib/Drupal/Core/Language/LanguageManagerInterface.php
    @@ -65,6 +65,20 @@ public function getDefinedLanguageTypesInfo();
    +   * @return \Symfony\Component\HttpFoundation\RedirectResponse
    

    Apparently the return value can also be FALSE, the docs don't say that.

  2. +++ b/core/modules/language/src/EventSubscriber/LanguageRequestSubscriber.php
    @@ -91,14 +92,33 @@ public function onKernelRequestLanguage(GetResponseEvent $event) {
    +          $event->stopPropagation();
    +          $event->setResponse($redirect);
    

    These can be chained.

  3. +++ b/core/modules/language/src/LanguageNegotiationMethodBase.php
    @@ -65,4 +65,10 @@ public function persist(LanguageInterface $language) {
    +    // Default implementation persists nothing.
    

    Copy/pasted comment that is nonsensical here.

  4. +++ b/core/modules/language/src/LanguageNegotiationMethodBase.php
    @@ -65,4 +65,10 @@ public function persist(LanguageInterface $language) {
    +  }
     }
    

    Needs newline in between.

  5. +++ b/core/modules/language/src/LanguageNegotiationMethodInterface.php
    @@ -62,4 +62,13 @@ public function getLangcode(Request $request = NULL);
    +   * plugin.
    ...
    +  public function reactSelectedLanguage(LanguageInterface $selected_language);
    

    Comment doesn't match what the method name says.

  6. +++ b/core/modules/language/src/LanguageNegotiator.php
    @@ -159,6 +170,22 @@ public function initializeType($type) {
    +  public function getNegotiationMethodsRedirect($type) {
    +    if (isset($this->unselectedNegotiationMethodes[$type])) {
    +      $language = $this->languageManager->getCurrentLanguage($type);
    

    This will break if this is called *before* LanguageNegotiator::initializeType() is called. And ::initializeType() is called when you call ::getCurrentLanguage(). So, it would be safer to move the last quoted line to run first, to avoid that edge case.

  7. +++ b/core/modules/language/src/LanguageNegotiator.php
    @@ -159,6 +170,22 @@ public function initializeType($type) {
    +      foreach ($this->unselectedNegotiationMethodes[$type] as $method_id) {
    

    Does it really make sense to iterate over all the higher-priority language negotiators that weren't able to negotiate a language?

    Doesn't this mean that we do the redirect in many more cases than only the browser-based negotiation? E.g. the user, session, etc. language negotiators, if they run after the URL-based negotiator, they will also redirects.

  8. +++ b/core/modules/language/src/LanguageNegotiatorInterface.php
    @@ -135,6 +135,18 @@ public function setCurrentUser(AccountInterface $current_user);
       public function initializeType($type);
     
    +
    +  /**
    

    One extraneous newline.

  9. +++ b/core/modules/language/src/LanguageNegotiatorInterface.php
    @@ -135,6 +135,18 @@ public function setCurrentUser(AccountInterface $current_user);
    +   * Gets enabled detection methods for the provided language type.
    ...
    +   * @return array
    +   *   An array of enabled detection methods for the provided language type.
    

    Docs are completely wrong.

  10. +++ b/core/modules/language/src/LanguageNegotiatorInterface.php
    @@ -135,6 +135,18 @@ public function setCurrentUser(AccountInterface $current_user);
    +  public function getNegotiationMethodsRedirect($type);
    

    This feels like a very weird/wrong method name. Why not just getRedirect()?

  11. +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationBrowser.php
    @@ -40,11 +40,6 @@ public function getLangcode(Request $request = NULL) {
    -      // Internal page cache with multiple languages and browser negotiation
    -      // could lead to wrong cached sites. Therefore disabling the internal
    -      // page cache.
    -      // @todo Solve more elegantly in https://www.drupal.org/node/2430335.
    -      \Drupal::service('page_cache_kill_switch')->trigger();
    
    +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
    @@ -101,6 +102,44 @@ public function getLangcode(Request $request = NULL) {
    +      // Don't cache this redirect as every future request has maybe
    +      // another language.
    +      \Drupal::service('page_cache_kill_switch')->trigger();
    

    We should be getting rid of the call to the page cache kill switch!

    Also: this new place for calling the page cache kill switch seems wrong: it is the *browser* language negotiation that causes this, not the URL language negotiation being asked to generate the URL for the browser language negotiation!

  12. +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
    @@ -101,6 +102,44 @@ public function getLangcode(Request $request = NULL) {
    +    // Do not redirect if one of the url prefixes is empty or the request is
    

    s/url/URL/

  13. +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
    @@ -101,6 +102,44 @@ public function getLangcode(Request $request = NULL) {
    +    // something else then GET.
    

    s/then/than/

  14. +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
    @@ -101,6 +102,44 @@ public function getLangcode(Request $request = NULL) {
    +    if (in_array('', language_negotiation_url_prefixes()) || $request->getMethod() != "GET") {
    

    GET or not: this belongs in a higher level.

  15. +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
    @@ -101,6 +102,44 @@ public function getLangcode(Request $request = NULL) {
    +    // Do not redirect if the request has the "destination" query parameter,
    +    // because it will override the target URL.
    +    /* @see \Drupal\Core\EventSubscriber\RedirectResponseSubscriber::checkRedirectUrl() */
    +    if ($request->query->has('destination')) {
    +      return FALSE;
    +    }
    

    Will this not have to be reimplemented by every single implementation of this interface method? i.e. shouldn't this live at a higher level?

  16. +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
    @@ -101,6 +102,44 @@ public function getLangcode(Request $request = NULL) {
    +    // \Drupal::urlGenerator() will run processOutbound() which will
    

    \Drupal::urlGenerator -> \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute()

  17. +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
    @@ -101,6 +102,44 @@ public function getLangcode(Request $request = NULL) {
    +    // adapt the URL with the url negotiation from the selected language.
    

    s/url/URL/

  18. +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
    @@ -101,6 +102,44 @@ public function getLangcode(Request $request = NULL) {
    +    unset($query['q']);
    

    Why is this necessary?

  19. +++ b/core/modules/language/src/Tests/LanguageListTest.php
    @@ -154,6 +154,9 @@ function testLanguageList() {
    +    // Drupal::url() will construct correct URL.
    

    s/Drupal::url()/\Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute/

    Also, which static cache is causing the need for this reset?

Wim Leers’s picture

Issue tags: +Needs tests
Leksat’s picture

To continue work on this I need to know something...

Here is the current call stack:
\Drupal\language\EventSubscriber\LanguageRequestSubscriber::onKernelRequestLanguageRedirect()
\Drupal\Core\Language\LanguageManagerInterface::getLanguageRedirect()
\Drupal\language\LanguageNegotiatorInterface::getNegotiationMethodsRedirect()
\Drupal\language\LanguageNegotiationMethodInterface::reactSelectedLanguage()

A redirect only can be performed only on some conditions like

$can_redirect = $request->getMethod() == "GET" && !$request->query->has('destination');

If we put this check on the top level (as suggested in 14 and 15 of #39), there will be no need to execute further methods if $can_redirect is FALSE. However, this also means that in this case the language negotiation methods will not be available to react on the selected language. Is that okay?

If yes, can we also rename reactSelectedLanguage() to getRedirect()? I think, that well be more logical.

Gábor Hojtsy’s picture

+++ b/core/modules/language/src/EventSubscriber/LanguageRequestSubscriber.php
@@ -91,14 +92,33 @@ public function onKernelRequestLanguage(GetResponseEvent $event) {
   static function getSubscribedEvents() {
-    $events[KernelEvents::REQUEST][] = array('onKernelRequestLanguage', 255);
-
+    $events[KernelEvents::REQUEST][] = array('onKernelRequestSetLanguage', 255);
+    $events[KernelEvents::REQUEST][] = array('onKernelRequestLanguageRedirect', 225);

I would be happy to somehow help reviewing this issue (or help with where redirect should happen on the call stack) but I totally fail to understand what is the proposed architecture. And some of the call stack you introduced...

Eg. this one suggests you have a language redirect subscriber that would run the negotiation before the negotiation itself happens?

I keep asking for an issue summary update to help me understand, if you know how the patch is architected, that would be a great task to start with so people can help :)

Leksat’s picture

I'm not sure if I can update the issue summary properly, but I can describe here in the comment what I have learned while working on this.

Initial problem:
Browser language negotiation always kills page cache.

Here is a behavior example for the case when a) URL negotiation is enabled and language prefixes are set up properly, b) browser negotiation is enabled and goes after the URL negotiation.

Before patch:
- "domain.com/" URL accessed
- "domain.com/" page cache is killed
- all links rendered on the page leads to language prefixed URLs

After patch:
- "domain.com/" URL accessed
- "domain.com/" page cache is killed
- user is redirected to language prefixed URL ("domain.com/en")

So, the patch does not avoid cache kill, instead it introduces language redirect. (Which in D7 was usually performed by globalredirect module.)

Current implementation:
(assuming ConfigurableLanguageManager is used)
- on request event
  - onKernelRequestSetLanguage() is called
    - language negotiator iterates over language negation methods
    - if language was detected, iteration stops
    - if not, the invoked method is stored in the unselectedNegotiationMethodes array
  - onKernelRequestLanguageRedirect() is called
    - language negotiator iterates over methods stored in the unselectedNegotiationMethodes array
    - if reactSelectedLanguage() returned redirect response, iteration stops, and the response object is passed up by the call stack to be set as a response for the current request

The call stack from #41 seems not logical to me, because the three first method names in the call stack mean "getting redirect", while the last one means "reacting on event".
I think, to make the code clear, we need to answer a question: what we try to achieve here? Allow language negotiation methods only to redirect, or allow them to react in any way they want to?

1. Allow language negotiation methods only to redirect
This is what we currently implemented. I think, to finish this, we just need to rename the methods and update them docs.

2. Allow language negotiation methods to react in any way they want to.
I think this is not really possible because in this case we'll have to use some equivalent of D7 drupal_goto() inside the URL negotiation method. I searched through the D8 core and did not find a place where the request-response flow breaks somewhere in the middle, except for \Drupal\Core\Form\EnforcedResponseException.

victor-shelepen’s picture

Could anybody look my approach that is described at the 7-th comment?

There is a proposition to modify cid: we need get the real page path, add a language key a separated key.

It returns the bare path.
$uri = $this->pathProcessor->processInbound($uri, $request);

It returns the current language according to Negotiators.
$language = $this->languageManager->getCurrentLanguage();

So the page cache becomes more flexible.

protected function getCacheId(Request $request) {
    $uri = $request->getRequestUri();
    $language = $this->languageManager->getCurrentLanguage();
    $langcode = $language->getId();
    $uri = $this->pathProcessor->processInbound($uri, $request);
    $langcodes = array_keys($this->languageManager->getLanguages());
    if($this->languageManager->isMultilingual()) {
      $parts = explode('/', $uri);
      if (in_array($parts[1], $langcodes)) {
        array_shift($parts);
        $langcode = array_shift($parts);
        $uri = implode('/', $parts);
      }
    }
    $cid_parts = array(
      $uri,
      $langcode,
      $request->getRequestFormat(),
    );

    return implode(':', $cid_parts);
  }

Thank you.

znerol’s picture

There is a proposition to modify cid: we need get the real page path, add a language key a separated key.

@likin: Regrettably this is not a good idea because initializing the language manager at this point in time will kill page cache performance. In addition, the redirect based approach is much better from a SEO perspective and fixes existing issues with proxy caches.

Also the session is not yet loaded at this point in time and thus session based content negotiation is not yet possible. There are ongoing efforts to push page caching even further up the stack at #2501989: [meta] Page Cache Performance.

Gábor Hojtsy’s picture

@Leksat: re #43, the steps explained made WAY EASIER to understand what is going on. I agree there is not much of a use case for language negotiators to react with anything else but a redirect.

Gábor Hojtsy’s picture

Issue summary: View changes
Gábor Hojtsy’s picture

@Leksat: can you work on an updated patch?

victor-shelepen’s picture

Status: Needs work » Needs review
FileSize
18.05 KB
4.32 KB

I've written the test. But the page is not redirected according to the logic above. For simple redirection it the big patch :)

Status: Needs review » Needs work

The last submitted patch, 49: 2430335-language-negotiation-redirect-49.patch, failed testing.

The last submitted patch, 37: 2430335-language-negotiation-redirect-37.patch, failed testing.

Leksat’s picture

Assigned: Schnitzel » Leksat

Working on it...

Leksat’s picture

Assigned: Leksat » Unassigned
Status: Needs work » Needs review
FileSize
17.7 KB
12.12 KB

Here is the patch including fixes for #39. I did not change the newly added test.

Leksat’s picture

If the approach from the current patch will be accepted, here are some thoughts to further improvements:
- Automatically add language prefix if second language was added. This will avoid page cache issues if URL negotiation is enabled.
- Report misconfigured language negation methods on the Status report page.
- The Session language negation method currently uses $_SESSION for storing the language. The GET query parameter is used only for switching language. Does the !empty($_SESSION) breaks page cache as it was in D7? If yes, we can also implement getLanguageRedirect() there and avoid $_SESSION usage.

Leksat’s picture

I've also tried another approach, but got no luck with it. Could someone review attached patch?
The idea:
- LanguageRequestSubscriber::onKernelRequestLanguageRedirect() constructs a URL for the current request
- it also adds the language option in it
- then it compares with the current request
- if it's different, a redirect is performed

Status: Needs review » Needs work

The last submitted patch, 54: 2430335-54.patch, failed testing.

victor-shelepen’s picture

Status: Needs work » Needs review
FileSize
9.75 KB

The new patch has been done according to the comments 49, 56. New approach from #56 with the test from #49.

Status: Needs review » Needs work

The last submitted patch, 58: 2430335-browser_language-58.patch, failed testing.

victor-shelepen’s picture

There is an idea to modify the UrlGenerator to reduce a number of redirections...

Leksat’s picture

Status: Needs work » Needs review
FileSize
6.89 KB

A bit improved version of #58. Let's see what tests say.

Status: Needs review » Needs work

The last submitted patch, 61: 2430335-61.patch, failed testing.

Leksat’s picture

Status: Needs work » Needs review
FileSize
11.71 KB
1.56 KB

Status: Needs review » Needs work

The last submitted patch, 63: 2430335-63.patch, failed testing.

Leksat’s picture

Status: Needs work » Needs review
FileSize
12.31 KB
4.92 KB
Leksat’s picture

Comments for #65:

I updated the new test to set "en" prefix for the English language. After that, the caching works as expected. But. If user do not know of this peculiarity, and the prefix for the default language is not set, the caching will not work.

I tried to fix this. I modified language_negotiation_url_prefixes_update() to always add prefixes, no matter if language is default or not. That worked, but too many tests rely on the fact that the default language has no path prefix. So, I think, we first need to get confirmation for this change (or resolve this issue in other way, for example, implement hook_requirements() that warns user about the cache issue if the path prefix is not set for the default language).

Also, I noticed that many tests use \Drupal\simpletest\WebTestBase::drupalGet() in a strange manner. They include the language path prefix to the $path like this:

    $this->drupalGet('en/some/page');

This is not really correct because the $path is passed to \Drupal\Core\Routing\UrlGenerator::generateFromPath() and is processed by URL outbound processors. Instead, it should be done like this:

    $this->drupalGet('some/page', array(
      'language' => $this->container->get('language_manager')->getLanguage('en'),
    ));

The first example code works because the default language has no path prefix (by default), so the prefix is not added to the $path, and the $path remains the same. If we configure the path prefix for the default language on any of such tests, it will fail.

Leksat’s picture

Issue summary: View changes

Updated summary.

Leksat’s picture

Updated tags.

Leksat’s picture

Issue summary: View changes
victor-shelepen’s picture

@Leksat Please attach interdiffs. Yes many some tests do not aware of the language prefix. But there are not so many tests. If we choose the redirection policy. We need changes related tests.

victor-shelepen’s picture

FileSize
8.63 KB

It is not a fact that it adds a prefix.

$this->drupalGet('language_test/type-link-active-class', array(
+      'language' => $this->container->get('language_manager')->getLanguage('fr'),
+    ));

I agree with @Leksat to add language prefixes always.

Gábor Hojtsy’s picture

Its a feature to not add prefixes at all times. People may set up a German site and not intend to make it multilingual, then having a /de prefix just in case they sometime become multilingual is a no-go. Adding the /de prefix once they become multilingual would be a SEO disaster, all links would change, there would be no redirects between them, etc. BTW you are experiencing problem C in the issue summary:

You may use browser negotiation without URL language detection. We need to somehow ensure that you only use browser language negotiation if we can redirect to a concrete URL that is explicit for the negotiated language (contrib negotiation methods may be involved, session negotiation may be fed with an URL parameter to set this, etc).

If an empty prefix both means browser negotiation and some site default language then the browser negotiation will obviously not be able to redirect to the site default language with URL prefixes.

Leksat’s picture

If there is only one language, the \Drupal\language\LanguageServiceProvider does not register URL inbound/outbound processors. So, even if language prefix is set, it isn't used.

Leksat’s picture

BTW, #73 means that the container should be rebuilt after the amount of languages is changed to/from 1. Or, better, when \Drupal\language\LanguageServiceProvider::isMultilingual() return value changes.

Gábor Hojtsy’s picture

@Leksat: right, so when you go multilingual all your URLs suddenly change if we enforce path prefixes then.

znerol’s picture

@Gábor Hojtsy: Not quite. The original URL will redirect to the page in the proper language, since language negotiation is performed on all non-prefixed URLs if I'm not completely mistaken.

Gábor Hojtsy’s picture

@znerol: what, how? If you have only one language, so no path prefix, you always get that language. If you then have multiple languages and all have prefixes and apply browser language negotiation like this issue plans, then you may or may not get the language you got before when visiting pages without a language prefix. What am I missing then?

Gábor Hojtsy’s picture

Issue tags: -sprint

Apparently it does not seem like people care enough about this issue at this time to make it a priority to work on. Removing from sprint.

Gábor Hojtsy’s picture

Status: Needs review » Needs work
Issue tags: +sprint

Ok, sat down with Leksat to review this in Barcelona:

  1. +++ b/core/modules/language/src/EventSubscriber/LanguageRequestSubscriber.php
    @@ -91,13 +124,43 @@ public function onKernelRequestLanguage(GetResponseEvent $event) {
    +   * Performs the language redirect if required.
    

    Better doc would be IMHO: "Performs a redirect if the path changed in routing.

    For example when language negotiation selected a different language for the page."

    (We believe the only example is this example BTW, that is why the method is in this class. But I think for example sounds appropriate for this).

  2. +++ b/core/modules/language/src/EventSubscriber/LanguageRequestSubscriber.php
    @@ -91,13 +124,43 @@ public function onKernelRequestLanguage(GetResponseEvent $event) {
    +      if ($redirect_uri != $original_uri) {
    +        $event->setResponse(new RedirectResponse($redirect_uri));
    +      }
    

    Sounds pretty generic indeed. I am not sure if we want to move this to somewhere else, because we indeed do it due to the language changes.

    I've been thinking hard about what kind of other things may change the URL in the routing process, but I don't think there is anything else.

    Are the query string values the only we need to carry over to options to ensure we generate the same URL?

  3. +++ b/core/modules/language/src/EventSubscriber/LanguageRequestSubscriber.php
    @@ -91,13 +124,43 @@ public function onKernelRequestLanguage(GetResponseEvent $event) {
    +    // Execute after routes are initialized in
    

    .

  4. +++ b/core/modules/language/src/Tests/LanguageListTest.php
    @@ -45,6 +45,8 @@ function testLanguageList() {
         $this->assertText('French', 'Language added successfully.');
    +    $this->rebuildContainer();
    +    \Drupal::service('router.builder')->rebuild();
    
    @@ -154,6 +156,8 @@ function testLanguageList() {
         $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
    +    $this->rebuildContainer();
    +    \Drupal::service('router.builder')->rebuild();
    

    Let's add comments on why we are doing this then, so people don't need to git blame for this issue :)

  5. +++ b/core/modules/language/src/Tests/LanguagePageCacheTest.php
    @@ -0,0 +1,131 @@
    + * It tests browser language detection and page caching.
    ...
    +   * It tests browser language detection and page caching.
    

    Test browser...

  6. +++ b/core/modules/language/src/Tests/LanguagePageCacheTest.php
    @@ -0,0 +1,131 @@
    +   * The added languages.
    

    Codes of the languages added.

  7. +++ b/core/modules/language/src/Tests/LanguagePageCacheTest.php
    @@ -0,0 +1,131 @@
    +   * @var array
    +   */
    +  protected $langcodes;
    ...
    +    $this->langcodes = array('fr');
    +    foreach ($this->langcodes as $langcode) {
    +      ConfigurableLanguage::createFromLangcode($langcode)->save();
    +    }
    ...
    +    $language = new Language(array('id' => ''));
    

    Would be nice to make this an array of languages instead, so we don't need to fake-create them again later. We already created them in the setup method, we can store them on the array keyed by langcode.

  8. +++ b/core/modules/language/src/Tests/LanguagePageCacheTest.php
    @@ -0,0 +1,131 @@
    +  protected function setupLanguages() {
    

    setUp...

  9. +++ b/core/modules/language/src/Tests/LanguagePageCacheTest.php
    @@ -0,0 +1,131 @@
    +    $this->assert($links['fr']['class'] == 'is-active', 'The "en" is defied');
    ...
    +    $this->assert($links['en']['class'] == 'is-active', 'The "en" is defied');
    ...
    +    $this->assert($links['fr']['class'] == 'is-active', 'The "fr" is defied');
    

    Feedback messages not correct English, not in line with what is being asserted.

  10. +++ b/core/modules/language/src/Tests/LanguagePageCacheTest.php
    @@ -0,0 +1,131 @@
    +  public function setCache($period=300) {
    

    Lacks phpdoc.

    setUpCacheMaxAge() IMHO otherwise setCache() sounds very generic. setUp... aligns with the setup method pattern.

  11. +++ b/core/modules/language/src/Tests/LanguagePageCacheTest.php
    @@ -0,0 +1,131 @@
    +  public function setLanguageNegotiationOrder() {
    

    setUpLan....

    phpdoc

  12. +++ b/core/modules/language/src/Tests/LanguagePageCacheTest.php
    @@ -0,0 +1,131 @@
    +  public function getPage($path, $langcode_browser_fallback='en') {
    

    phpdoc

  13. +++ b/core/modules/language/src/Tests/LanguagePageCacheTest.php
    @@ -0,0 +1,131 @@
    \ No newline at end of file
    

    :/

karmazzin queued 65: 2430335-65.patch for re-testing.

The last submitted patch, 61: 2430335-61.patch, failed testing.

karmazzin’s picture

Status: Needs work » Needs review
FileSize
13.02 KB
6.95 KB

I think these changes should help to close this issue.

Status: Needs review » Needs work

The last submitted patch, 82: interdiff-65-82.patch, failed testing.

The last submitted patch, 82: 2430335-82.patch, failed testing.

The last submitted patch, 65: 2430335-65.patch, failed testing.

karmazzin’s picture

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

Previous patch revealed a problem in the test that came from https://www.drupal.org/node/1810394. I think it's good that we managed to catch it at an early stage.

Leksat’s picture

Great work @karmazzin!
I did some minor changes. The patch looks good from my side.
I also created an interdiff to latest reviewed patch (#65). Can someone review the patch again?

Leksat’s picture

Form 2. of #79:

+++ b/core/modules/language/src/EventSubscriber/LanguageRequestSubscriber.php
@@ -91,13 +124,43 @@ public function onKernelRequestLanguage(GetResponseEvent $event) {
+      if ($redirect_uri != $original_uri) {
+        $event->setResponse(new RedirectResponse($redirect_uri));
+      }

Sounds pretty generic indeed. I am not sure if we want to move this to somewhere else, because we indeed do it due to the language changes.

I've been thinking hard about what kind of other things may change the URL in the routing process, but I don't think there is anything else.

Are the query string values the only we need to carry over to options to ensure we generate the same URL?

To answer these questions I'd like to prepare another patch that will do the same redirect, but in the system module, so all tests are affected. We'll see then how much of tests will fail and why.

Leksat’s picture

Okay, let's see what will happen if we perform redirect from the system module.

Status: Needs review » Needs work

The last submitted patch, 89: 2430335-89-perform_redirect_in_sysyem_module.patch, failed testing.

Leksat’s picture

So, if we always perform this redirect, it fails some tests. My thoughts on this:
1. If tests fail in redirect-always mode, I would not recommend to commit the patch, because it may cause bugs on Drupal installations having language module enabled.
2. I would have this redirect in the system module, so it works always and all tests affected.

If I have time, I'll check test fails. Maybe it's just hidden bugs, like the one @karmazzin fixed in #86.

Gábor Hojtsy’s picture

Issue tags: -sprint

Removing from D8MI sprint due to lack of attention.

The last submitted patch, 89: 2430335-89-perform_redirect_in_sysyem_module.patch, failed testing.

Leksat’s picture

Hello all!

Removing from D8MI sprint due to lack of attention.

This issue never had lack of my attention. And recently I had some time during the nights to work on it, thanks to my newborn son Jan :)

So, I continued with #89 approach. I worked in a separate issue to not mess things up and to not bother people.

Feedback and reviews are welcome! #2641118: Route normalizer: Global Redirect in core

victor-shelepen’s picture

@Leksat, please, merge a test into your patch from #59. Thank you.

Leksat’s picture

@likin, #2641118: Route normalizer: Global Redirect in core has its own tests including one for language redirect.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

gg4’s picture

Version: 8.1.x-dev » 8.2.x-dev

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

hostedpower’s picture

Hi,

Was this fixed or not? I'm using Drupal 8.1.8 and seem to have issues on our website: https://www.hosted-power.com/

When visiting with English preference it still shows dutch :(

hostedpower’s picture

Any update on this? Upgraded to 8.2 but still the browser language isn't taken into account it seems :|

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

drikc’s picture

I'm having an issue where in 8.2.x paths like /example/photos were resolving correctly without redirecting (with a language prefix).

Now, since 8.3.x, that /example/photos path render a 404. Sometime, when I clear the cache the path resolve as /en/example/photos and render correctly but after a while it doesn't and return a 404!!!

Does someone have a quick solution to redirect any non language prefixed paths to a prefixed one? Load of urls are actually rendering 404 because of this behavior change since 8.2.x to 8.3.x!!!

In this urgent situation I'm trying to redirect in checkForRedirection event as explain here https://www.drupal.org/node/2013014 but without satisfying solution...

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

smk-ka’s picture

Just a heads-up, #89 breaks image style generation, since the generated route looks different from the input route, and therefore triggers a redirect. Example:
Input route: /sites/default/files/styles/image_style/public/image.png?itok=abcdef
Generated route: /sites/default/files/styles/image_style/public?itok=abcdef&file=image.png.
The first image derivate after a cache reset will still be generated, after that the directory exists and Apache immediately responds with a 404. I think, this will break all routes that accept arbitrary data, this one is just the most obvious.

Berdir’s picture

That patch there is very old and moved to #2641118: Route normalizer: Global Redirect in core as commented a bit later, and that in turn has been implemented and added to the redirect.module, which fixed that problem and many others by now. So, if you want this, just use redirect.module for now.

smk-ka’s picture

Good to know, thanks!

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

sleitner’s picture

The tests of the language module do not test what happens if "Accept-Language" is missing in the browser headers (missconfigured browsers or bots). The browser language detection skips to the default language and this is cached.

Subsequent requests with a correct "Accept-Language" header are answered with the cached default language and the browser language detection is not taken into account. This behavior is not mentioned anywhere and makes browser language detection unusable.

See #2986325: Browser language detection broken as soon as a request with no Accept-Language header happens for a test that demonstrates this.

valthebald’s picture

Issue tags: +Needs tests
FileSize
2.78 KB

(Scratching my own itch here, so addressing only C and no test for now):

What if instead of `KillSwitch` caching policy there would be a new `Vary` policy? In this case browser negotiator can call it to make response vary on accept language header.

PoC attached

valthebald’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests

Add a test, and modify PageCache behavior

valthebald’s picture

Status: Needs review » Needs work

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

sleitner’s picture

@valthebald I tested your #112 patch and this one works a little bit better than master for my case with an empty "Accept-Language" header:
the previous language is delivered, but it should be the selected default language.
See test attached: LanguageBrowserDetectionEmptyTest.txt

valthebald’s picture

Status: Needs work » Needs review
FileSize
2.84 KB
10.11 KB

@sleitner I'd argue that the current behavior is correct - language negotiators are chain-processed by `LanguageNegotiator` class, so if browser detection doesn't return any language, the next negotiator should be asked.

In the attached patch I've fixed failing tests - they fail because PageCache treats Vary: Cookie in a distinct way.

valthebald’s picture

Issue summary: View changes
valthebald’s picture

Issue summary: View changes

Status: Needs review » Needs work

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

valthebald’s picture

Fixed CS and added test-only patch

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

Status: Needs review » Needs work

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

valthebald’s picture

Status: Needs work » Needs review
FileSize
15.68 KB

@sleitner please ignore my previous comment - I got your point about reacting to empty Accept-Language header, and added your test (with slightly different name, because it tests different combinations of Accept-Language, not only empty one).

I've addressed this case by unconditionally adding 'Vary: Accept-Language' in `LanguageNegotiationBrowser::getLangcode()`

Also, I've added cache header assertion to the second call with French browser

valthebald’s picture

Status: Needs review » Needs work

The last submitted patch, 124: 2430335-123-testonly.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

valthebald’s picture

Status: Needs work » Needs review

Back to needs review because the last patch was expected to fail

sleitner’s picture

@valthebald your patch in #123 works with phpunit and in normal browser as expected

sleitner’s picture

Status: Needs review » Reviewed & tested by the community
Lukas von Blarer’s picture

Status: Reviewed & tested by the community » Needs review

I guess we'll need the test to turn green before this can be RTBC.

Does anyone use this patch in production?

Status: Needs review » Needs work

The last submitted patch, 124: 2430335-123-testonly.patch, failed testing. View results

Lukas von Blarer’s picture

This patch need a re-roll.

andypost’s picture

Issue tags: +Needs reroll
jofitz’s picture

Re-rolled and fixed a couple of coding standards errors.

The last submitted patch, 133: 2430335-133-test_only.patch, failed testing. View results

valthebald’s picture

@Lukas von Blarer: we use the patch from #123 on production (with core 8.6.2) - works nice with page cache module and varnish

catch’s picture

I don't think we can use Vary: here, and have pushed back against it being used in other places in core.

The Accept Language: header is extremely flexible as to what it contains:

Accept-Language: <language>
Accept-Language: <locale>
Accept-Language: *

// Multiple types, weighted with the quality value syntax:
Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language

This means that what ends up getting resolved to 'French' in the response, could be dozens of distinct headers sent by the browser.

Here's a fastly blog on having to normalize the Accept-Encoding header because they found 44(!) different variations for something which can only result in two different responses: https://www.fastly.com/blog/best-practices-using-vary-header

Similarly Varnish recommends normalizing Accept-Encoding:

https://varnish-cache.org/docs/3.0/tutorial/vary.html

And Accept-Encoding only has two real consequential values as such, language is actually designed to be flexible.

For example, on my own machine:

Safari: Accept-Language: en-gb

Chrome: Accept-Language: en-US,en;q=0.9,pt;q=0.8

These are semantically completely different even though on 99.99% of sites they'll result in the same response. Drupal does support differentiating between en-US and en-gb, but without actually running language negotiation there's no way to know whether it does or not, by which point the page cache response is doing too much work.

On top of this a lot of example varnish vcls disable the vary header because different frameworks set things like User-Agent, so if we start to rely on it, people may get wrongly cached items.

Conversely, with the patch as it stands, it could result in hundreds or thousands of cache entries per page - worse than not being cached.

The older approaches on here which do a redirect (to a page which might be page cached) give us something predictable, although not sure that's better than the status quo.

valthebald’s picture

@catch: I don't see how normalizing of Accept-Language contradicts with having Vary: in backend response.

1. If varnish (or any other reverse proxy) reduces number of possible header values, good - less variations in page cache. If not - page cache behaves at least not worse than with the current kill switch behavior.
2. Cache guide (https://www.drupal.org/docs/8/api/cache-api/cache-contexts) says very clearly that

Cache contexts are analogous to HTTP's Vary header.

3. Core already contains HeadersCacheContext context, and the only (known to me) way to tell reverse proxies that response is varied by the value of (some) header is to use Vary header.

In other words, we have fully HTTP compliant mechanism of page cache context. Why exactly not use it?

sleitner’s picture

@catch The status quo browser language detection completely does not work without patches ( #2430335-110: Browser language detection is not cache aware )

As @valthebald wrote in the issue description, the redirect to the language URL will be handled by #2641118: Route normalizer: Global Redirect in core and this works with #2430335-133: Browser language detection is not cache aware . The cache size is smaller, because only the redirect is saved, not the complete page HTML.

On my test server the Vary approach is faster (35-40 ms; compared to kill_switch: 60-80ms).

In most cases only one page will be affected by the huge browser language detection cache: the front page / .

We could add an option to switch between

  • the vary approach for big pages with proxy and header normalizer
  • the patched kill_switch approach for smaller pages

on /admin/config/regional/language/detection/browser

Berdir’s picture

I still think that the correct approach is to enforce a single canonical URL which is what happens with browser negotation in combination with the redirect module, see also #106.

valthebald’s picture

@Berdir: totally agree. But this is a different issue - RedirectResponse is cacheable, and without fixing this issue global redirect in core will return wrong result. I.e. in our live set up we have:

  1. Varnish that normalizes Accept-Language to limited set of languages (number of languages on the site)
  2. Redirect module
  3. This patch

Url language negotiation comes before browser negotiation.

When user goes to `/node/`:

- LanguageNegotiationBrowser returns preferred user's language, say 'fr', and adds Vary:Accept-Language.
- Redirect module returns RedirectResponse to `/fr/alias-francaise` which is canonical URL for the french translation of the node. RedirectResponse is cacheable, and its metadata has 'header' context)

When user goes to `/fr/alias-francaise`:
- LanguageNegotiationUrl returns fr before browser detection.
- Response cached by page cache does not have 'header' context, and doesn't add 'Vary' header

catch’s picture

1. If varnish (or any other reverse proxy) reduces number of possible header values, good - less variations in page cache. If not - page cache behaves at least not worse than with the current kill switch behavior.

If they normalize it's fine. If they don't it's potentially a very low cache hit rate which can be worse than an uncacheable page, or conversely some online examples ignore the Vary header altogether. The page cache itself doesn't do any such normalisation.

2. Cache guide (https://www.drupal.org/docs/8/api/cache-api/cache-contexts) says very clearly that

Cache contexts are analogous to HTTP's Vary header.

Being analogous doesn't mean that actually using the Vary header is recommended, as above even Accept-Encoding is not sent consistently by browsers.

3. Core already contains HeadersCacheContext context, and the only (known to me) way to tell reverse proxies that response is varied by the value of (some) header is to use Vary header.

If you use HeadersCacheContext without specifying a particular header, it'll vary by the cookie header, meaning caching per session.

Url language negotiation comes before browser negotiation.

When user goes to `/node/`:

- LanguageNegotiationBrowser returns preferred user's language, say 'fr', and adds Vary:Accept-Language.
- Redirect module returns RedirectResponse to `/fr/alias-francaise` which is canonical URL for the french translation of the node. RedirectResponse is cacheable, and its metadata has 'header' context)

Assuming that the redirect is implemented (which I agree is what we should do overall), it comes down to the difference between a poorly cacheable RedirectResponse and an uncacheable one (or if we implemented some header normalisation in Drupal, a slightly slower but more cacheable RedirectResponse). Has anyone done a timing comparison for that?

valthebald’s picture

I had similar results as @sleitner in #138 - cached redirect response is 2 to 3 times faster than kill_switch. Results may vary (pardon the pun) of course, but the boost is caused in skipped DrupalKernel and language negotiation chain loading.

Regarding normalization of Accept-Language - it would be possible to perform inside Drupal (new middleware class in language module, that runs before PageCache in the stack?). Tricky part would be to make it as light as possible, and probably optional (via settings.php)

valthebald’s picture

Browser language negotiation is first, but not the only one piece of functionality that could win from the new Vary cache policy. Another example from recent real-life project:

We have a block plugin that should show different content depending on visitor's country. On production, we have Cloudflare CDN that sends `CF-IPCountry` header, and in other environments custom geo-ip based service that emulates Cloudflare behavior.

Without vary cache policy, only implementation that we could come with, was using placeholders and AJAX loading of those blocks. With vary policy, solution could be more elegant and not require AJAX calls (or BigPipe)

I wonder would it be better to spin off creation of Vary cache policy to separate issue?

Berdir’s picture

Related problem that some of you might have been struggling with when it didn't seem to work at all: #3024535: LanguageNegotiationBrowser needs to trigger the page cache kill switch even when there is no Accept-Language header

sleitner’s picture

@Berdir : I already mentioned this problem six months ago in comment #110 #2986325: Browser language detection broken as soon as a request with no Accept-Language header happens with the same solution. I also built a custom module for one of my projects, because I do not want to patch against core forever.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Lukas von Blarer’s picture

Does anyone have a working patch for 8.6.x? The last few patches don't apply anymore...

markdc’s picture

@Lukas von Blarer: I'm still using #133 on the latest 8.6.13.

stBorchert’s picture

Attached you'll find a re-roll for 8.7.x in case you need this after updating to the new release.

Status: Needs review » Needs work

The last submitted patch, 149: 2430335-149-8.7.x.patch, failed testing. View results

Lukas von Blarer’s picture

The patch in #149 doesn't fix the issue anymore. The previous one was from #123 and that worked.

aschiwi’s picture

Btw, using the patch in #149 broke the browser language functionality for us. Removing the patch seems to work fine with just default Drupal settings.

Lukas von Blarer’s picture

@aschiwi no, without the patch this issue persists.

clairedesbois@gmail.com’s picture

The patch #149 works correctly for us.

clairedesbois@gmail.com’s picture

In fact, we had failed our tests and the patch #149 doesn't pass. And in fact, remove patch for this issue work perfectly. We are on Drupal 8.7.4.

clairedesbois@gmail.com’s picture

Lukas von Blarer’s picture

@aschiwi @Calystod Do you have page cache enabled?

aschiwi’s picture

Weird! Not sure why but with the patch we had a huge problem: the browser language functionality did not work at all. When we removed the patch, it started working again.

We do have Page cache enabled and it seems to continue to work. I can't publicly share the URL but pm me and I will share.

It took us a while to find out why browser language detection did not work anymore so once we found out that removing the patch solves the problem, we stopped looking.

Lukas von Blarer’s picture

@aschiwi Ok, i'll text on my project next week. Thank you!

Lukas von Blarer’s picture

I can confirm that this issue is fixed on 8.7.5 without this patch. I am about to test in production with varnish, but I guess it should be fine.

Can we close this issue or are there some improvements that still should go in? Currently there is no Vary header for example.

Edit: I didn't mean to upload that screenshot. You can ignore that.

Driskell’s picture

Using 8.7.1 and varnish cache the browser language detection is working - but it kills the cache. All content pages trigger UNCACHEABLE.
Do you mean to say 8.7.5 potentially fixes it?

bakulahluwalia’s picture

I have one site which has both varnish and cloudflare enabled. Settings for detection is:
1) URL
2) Browser lang
3) Default

Website is showing unexpected behavior. varnish and cloudflare both are not respecting the Browser lang at all. Drupal Version: 8.7.7

Details about the unexpected behavior:
Case1:
1) First visit site in russian language (Varnish will cache the page). Make sure it's homepage followed by the prefix /ru
2) Now, In another browser open the homepage (default should be english), make sure it is via varnish. it should not be a miss.
Result: the homepage will appear in russian instead of english(en is default in this case)
Case2:
1) Change browser setting to russian default lang. Visit the site. (Varnish will cache the data)
2) Open the site in different browser, english default lang. It will be served by varnish with russian content.

I have not tried the patch yet but I believe this issue can be fixed on varnish and cloudflare level itself using configuration.
I am just curious to see it working in action as it's core feature on our D8 and it doesn't work with varnish and cloudflare properly.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

andypost’s picture

andypost’s picture

FileSize
2.66 KB
11.12 KB

Fix one test and a bit of clean-up

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Falco010’s picture

@andypost I tested your patch, however it does not seem to fix it for us:
- Visit homepage in English browser
- Page is cached in English
- Visit website in Dutch browser
- Now all users get this English home page, no matter what language their browser is

varnish cache

We are considering to remove browser detection language and switching to Varnish Geoip.

nikitagupta’s picture

Assigned: Unassigned » nikitagupta
nikitagupta’s picture

Assigned: nikitagupta » Unassigned
shobhit_juyal’s picture

Hi, andypost

Due to some issue the patch #165 was not applying into 9.1.

I recreated it, but as per @Falco010 comment, it still the cache still persists.

The below piece of patch was creating issue to apply in #165:

diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationBrowser.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationBrowser.php
index abee150c57..80a3f8c32f 100644
--- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationBrowser.php
+++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationBrowser.php
@@ -47,7 +47,7 @@ public static function create(ContainerInterface $container, array $configuratio
    */
   public function getLangcode(Request $request = NULL) {
     $langcode = NULL;
-
+    \Drupal::service('page_cache_vary')->add('Accept-Language');
     if ($this->languageManager && $request && $request->server->get('HTTP_ACCEPT_LANGUAGE')) {
       $http_accept_language = $request->server->get('HTTP_ACCEPT_LANGUAGE');
       $langcodes = array_keys($this->languageManager->getLanguages());
@@ -55,12 +55,6 @@ public function getLangcode(Request $request = NULL) {
       $langcode = UserAgent::getBestMatchingLangcode($http_accept_language, $langcodes, $mappings);
     }
 
-    // Internal page cache with multiple languages and browser negotiation
-    // could lead to wrong cached sites. Therefore disabling the internal page
-    // cache.
-    // @todo Solve more elegantly in https://www.drupal.org/node/2430335.
-    $this->pageCacheKillSwitch->trigger();
-
     return $langcode;
   }
shobhit_juyal’s picture

Gábor Hojtsy’s picture

Use event-appropriate tag. (Sorry for the noise).

Lukas von Blarer’s picture

bserem’s picture

Status: Needs work » Needs review

Patch #173 applies nicely with composer, even though it fails to apply on the drupal.org test system.

It looks ok on our test environment, will do more test and provide even more feedback next week.

andypost’s picture

Status: Needs review » Needs work

It needs work because comment 167 and tests should pass

jasonawant’s picture

When evaluating the patch from 165, I can confirm results described in 167.

And, just want to share an alternate setup suggested by Berdir.

Instead of using this patch, I can use the Redirect's modules "Enforce clean and canonical URLs." option, which is described as

Enabling this will automatically redirect to the canonical URL of any page. That includes redirecting to an alias if existing, removing trailing slashes, ensure the language prefix is set and similar clean-up.

With the following configuration

  • All languages include path prefixes
  • Using both URL and Browser detection
  • Browser detection occurs after URL detection
  • Redirect's modules "Enforce clean and canonical URLs." option

products the following results

  • Requests for path without language prefix without Accept-Language request header are (301, per Redirect configuration) redirected to site default language prefixed path; the 301 redirect response has the following response header, Cache-Control: must-revalidate, no-cache, private
  • Requests for path without language prefix with Accept-Language request header and value mapped via Browser Detection configuration are (301, per Redirect configuration) redirected to the mapped language prefixed path; the 301 redirect response has the following response header, Cache-Control: must-revalidate, no-cache, private and the language prefixed path response is cacheable, Cache-Control max-age=2764800, public

I did not evaluate the patches from #2641118: Route normalizer: Global Redirect in core.

jozzy_a’s picture

#173 failed to apply against 8.9.2

 - Installing drupal/core (8.9.2): Loading from cache
  - Applying patches for drupal/core
    https://www.drupal.org/files/issues/2020-06-19/2430335-173-8.9.patch (#2430335 Browser language detection is not cache aware)
   Could not apply patch! Skipping. The error was: Cannot apply patch https://www.drupal.org/files/issues/2020-06-19/2430335-173-8.9.patch

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

dorficus’s picture

Rerolled the 8.9 patch in #173 to apply.
DO NOT TRY THIS PATCH

dorficus’s picture

I rerolled the patch and it applied to 8.9.6, however I'm now getting an error:

  [Symfony\Component\DependencyInjection\Exception\LogicException]                                                                                                     
  Service 'page_cache_vary' for consumer 'drupal.proxy_original_service.page_cache_response_policy' does not implement Drupal\Core\PageCache\ResponsePolicyInterface. 

I've checked, and the class is there and it's implementing the interface, however I can't figure out what is generating this error. Can anyone else test this patch with 8.9.7 and tell me if they get the same result? Or, does anyone know a workaround?

anmolgoyal74’s picture

Status: Needs work » Needs review
FileSize
11.11 KB

Re-rolled #165 for 9.2.x.

dorficus’s picture

Sorry, my previous patch was missing things that I should have noticed ahead of time. Apologies for the previous 2 comments.

The last submitted patch, , failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 182: 2430335-reroll-181-8.9.patch, failed testing. View results

dorficus’s picture

I'm not sure about the failing tests, but I am getting an error where it appears that the service is not available outside of the classes which seems to stem from the

  page_cache_vary:
    class: Drupal\Core\PageCache\ResponsePolicy\Vary
    public: false

part of the patch. I'm going to remove the public: false line in the patch and resubmit it.

dorficus’s picture

I should ask is there any reason for the service to be private?

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Haven't figured out the issue but during the test

foreach ($vary_headers as $vary_header) { Never seems to get called as $vary_headers appears empty.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

aludescher’s picture

Up to patch #165 the page_cache_kill_switch trigger was removed.
Starting with #173 the kill switch trigger is not removed i.e. it is still called, even though page_cache_vary is used (with the exception of #181).
Here's a rewrite of #190 without calling the page_cache_kill_switch trigger.

smustgrave’s picture

@aludescher thanks for the patch but please also upload an interdiff so people can see the changes you made.

Fixed the CI issue.

Status: Needs review » Needs work

The last submitted patch, 194: 2430335-194.patch, failed testing. View results

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

handkerchief’s picture

Any news on that?

Vadym.Tseiko’s picture

Hi everyone, I have a similar problem, my project is configured via detection and selection URL by domains method and I need to switch language to the correct domain accordingly to the user's browser preferences. But in my case, when using detection and selection via browser I'm constantly switched to my preferred language without the ability to switch language only once(what I'm achieved with custom event subscriber and redirect). I added a custom code that redirects user but now I'm having a problem with SEO as Google search bots is redirected too, and all machine services return 302 status code besides the basic English one as those bots I guess have English as a preference in their requests. Is there a way for redirect to not mess with bots or to detect them and not redirect if that not human-users, none of those patches fixed behavior when the user is constantly redirected and none of them set the right domain in the URL as a result I getting always URL from with browser switch to the preferred language? Has anyone solved this problem somehow?

Matthijs made their first commit to this issue’s fork.

Matthijs’s picture

Since the cache ID is actually cached, the $vary_headers passed to \Drupal\page_cache\StackMiddleware\PageCache::getCacheId() actually did nothing.

I created a merge request from #2430335-194: Browser language detection is not cache aware and did some changes to cache the cache ID by $vary_headers.

sleitner’s picture

I fixed the visibility of the modules variable of the test. Drupal\Tests\language\Functional\LanguageBrowserDetectionAcceptLanguageTest still fails.

sleitner’s picture

Status: Needs work » Needs review

X-Drupal-Cache is now always present. It is MISS for the first access and HIT for further accesses.

smustgrave’s picture

Status: Needs review » Needs work

Left some comments on MR

I don't know if adding a CR from D9 is correct anymore but will leave as is.

sleitner’s picture

Status: Needs work » Needs review
andypost’s picture

added 2 questions to MR

sleitner’s picture

I don't know why this is not a local variable. But since it only reads the request, a clone is not necessary.

smustgrave’s picture

Believe all feedback has been addressed around MR but don't really follow the issue summary, specifically the proposed solution section. Will leave in review if anyone else wants to pick up.

znerol’s picture

Status: Needs review » Needs work

This includes #3023104. As one of the authors of the cache policy mechanism, I'd be disappointed if this approach would make it into core. Citing from #3023104-4: Introduce "Vary" page cache response policy:

Regrettably this approach clearly violates the ResponsePolicyInterface contract. While not stated explicitly in the documentation, the check() method is certainly not expected to actually change the response.

znerol’s picture

I suggest to move the logic into a response event subscriber instead.