\Drupal\language\LanguageNegotiator does not handle PluginNotFoundException and break the site completely

Created on 7 May 2020, over 4 years ago
Updated 12 April 2023, over 1 year ago

Problem/Motivation

Drupal\language\LanguageNegotiator::initializeType does not handle Drupal\Component\Plugin\Exception\PluginNotFoundException at all. If a language negotiation plugin was removed by mistake or uninstalled improperly, the whole Drupal site will fail. All drush command or page rendering would be stopped by the issue.

Proposed resolution

I think the negotiation process should handle a missing plugin more gracefully. It should either simply log the error with watchdog or display a message on frontend to prompt user to handle it. This should not have been a site breaking problem.

Add a messenger argument to language.services.yml to handle exception with plugin missing message.

Remaining tasks


Add test case
code review

User interface changes

None

API changes

None

Data model changes

None

Release notes snippet

None

🐛 Bug report
Status

Fixed

Version

9.5

Component
Language system 

Last updated 2 days ago

  • Maintained by
  • 🇩🇪Germany @sun
Created by

🇭🇰Hong Kong yookoala

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇺🇸United States smustgrave

    This issue is being reviewed by the kind folks in Slack, #needs-review-queue-initiative. We are working to keep the size of Needs Review queue [2700+ issues] to around 400 (1 month or less), following Review a patch or merge request as a guide.

    As a bug this will need a test case.

  • 🇩🇪Germany sleitner

    Test only patch.

  • Status changed to Needs review almost 2 years ago
  • 🇩🇪Germany sleitner

    #21 and #24 combined, please review

  • Status changed to Needs work almost 2 years ago
  • 🇺🇸United States smustgrave

    New parameter should default to null with a trigger_error that it will be required in D11. Would search the repo for exact working. Then a simple unit or kernel test that when you pass null to the constructor you get that trigger_error response.

    Also with a new parameter it will need a change record to announce this constructor now takes a new parameter.

    Thanks

  • 🇬🇧United Kingdom joachim
    1. +++ b/core/modules/language/src/LanguageNegotiator.php
      @@ -130,7 +142,17 @@ public function initializeType($type) {
      +            // If a plugin is not found, simply log the error so user may handle it.
      

      Comment is too long, should be wrapped.

      Also, we're not 'just logging the error' since we're also showing an error message.

    2. +++ b/core/modules/language/tests/src/Functional/LanguageNegotiatorPluginTest.php
      @@ -0,0 +1,45 @@
      +    $this->assertSession()->statusCodeEquals(200);
      

      If we're only testing the status, could this be done in a kernel test?

  • Status changed to Needs review over 1 year ago
  • 🇩🇪Germany sleitner

    Please review, covers #28 and #29

  • Status changed to Needs work over 1 year ago
  • 🇪🇸Spain penyaskito Seville 💃, Spain 🇪🇸, UTC+2 🇪🇺
    1. +++ b/core/modules/language/src/LanguageNegotiator.php
      @@ -70,6 +72,13 @@ class LanguageNegotiator implements LanguageNegotiatorInterface {
      +   * Messenger Service.
      

      We use

         * The messenger.
      

      everywhere in Drupal core. Change to that.

    2. +++ b/core/modules/language/src/LanguageNegotiator.php
      @@ -70,6 +72,13 @@ class LanguageNegotiator implements LanguageNegotiatorInterface {
      +  protected $messenger;
      

      Nitpick: Put $messenger after $requestStack (before $currentUser and $negotiatedLanguages). I wouldn't be so nitpicky if I didn't have more feedback.

    3. +++ b/core/modules/language/src/LanguageNegotiator.php
      @@ -83,13 +92,20 @@ class LanguageNegotiator implements LanguageNegotiatorInterface {
      +   *   Messenger Service.
      

      Same here. Use "The messenger"

    4. +++ b/core/modules/language/src/LanguageNegotiator.php
      @@ -83,13 +92,20 @@ class LanguageNegotiator implements LanguageNegotiatorInterface {
      +      @trigger_error('Calling ' . __METHOD__ . ' without the $messenger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3280207', E_USER_DEPRECATED);
      

      The linked change record is unrelated. We need to create a draft one for the changes in this issue and link it here.

    5. +++ b/core/modules/language/src/LanguageNegotiator.php
      @@ -130,7 +146,17 @@ public function initializeType($type) {
      +            watchdog_exception('language', $e);
      

      We can use \Drupal::logger($type) instead?

    6. +++ b/core/modules/language/tests/src/Kernel/LanguageNegotiatorPluginTest.php
      @@ -0,0 +1,40 @@
      +  public function testLanguageNegotiatorNoPlugin() {
      

      This test has no assertions.

      I suggest:

      + We assert we don't have the exception we had until now.
      + We assert the message is shown.
      + We assert the error is logged.

    I also tempt to agree with @joachim at #16. A visitor could see this error which is scary and he can't do anything about it. We might want to just log it.

  • 🇮🇳India sahil.goyal

    Addressing the suggestions of #31 fixing these issues, Only point #4 is left as i not clear with it completely. Apart all issues being addresses in attached Patch with interdiffs. Leaving issue still in NW.

  • Status changed to Needs review over 1 year ago
  • Status changed to Needs work over 1 year ago
  • 🇺🇸United States smustgrave

    Was mentioned in #32 that #31.4 has not been addressed.

  • Status changed to Needs review over 1 year ago
  • 🇩🇪Germany sleitner

    Removed the messenger, just logs the error. No change record needed any more, because interface does not change.

  • Status changed to Needs work over 1 year ago
  • 🇪🇸Spain penyaskito Seville 💃, Spain 🇪🇸, UTC+2 🇪🇺

    #35 looks much better.

    1. +++ b/core/modules/language/tests/src/Kernel/LanguageNegotiatorPluginTest.php
      @@ -0,0 +1,52 @@
      +use ColinODell\PsrTestLogger\TestLogger;
      

      TIL :-)

    2. +++ b/core/modules/language/tests/src/Kernel/LanguageNegotiatorPluginTest.php
      @@ -0,0 +1,52 @@
      +    // Assert we don't have the exception we had until now.
      +    $this->assertNotInstanceOf('\Drupal\Component\Plugin\Exception\PluginNotFoundException', $switchLinks);
      

      I doubt this is the right assertion, as the exception would be thrown, not returned.
      Can we upload a test-only patch to verify?

  • 🇩🇪Germany sleitner

    I deleted the assertNotInstanceOf

  • Status changed to Needs review over 1 year ago
  • Status changed to RTBC over 1 year ago
  • 🇪🇸Spain penyaskito Seville 💃, Spain 🇪🇸, UTC+2 🇪🇺
    +++ b/core/modules/language/tests/src/Kernel/LanguageNegotiatorPluginTest.php
    @@ -0,0 +1,50 @@
    +    $this->assertFalse($logger->hasErrorThatContains('Valid plugin IDs for Drupal\language\LanguageNegotiationMethodManager are'));
    

    I'd prefer to assert by "The "Drupal\Tests\language\Kernel\LanguageNegotiatorPluginTest" plugin does not exist." but don't want to nitpick. Is RTBC for me.

    Thanks for your work on this!

  • Status changed to Needs work over 1 year ago
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10
    +++ b/core/modules/language/tests/src/Kernel/LanguageNegotiatorPluginTest.php
    @@ -0,0 +1,50 @@
    +    $this->assertFalse($logger->hasErrorThatContains('Valid plugin IDs for Drupal\language\LanguageNegotiationMethodManager are'));
    

    is assert false right here?

    The comment says 'assert the error is logged'

    but then we assertFalse?

  • 🇪🇸Spain penyaskito Seville 💃, Spain 🇪🇸, UTC+2 🇪🇺

    My bad, overlooked:

    1. +++ b/core/modules/language/tests/src/Kernel/LanguageNegotiatorPluginTest.php
      @@ -0,0 +1,50 @@
      +    $this->container->get('logger.factory')
      +      ->get('package_manager')
      +      ->addLogger($logger);
      

      This needs to be language, not package_manager.

      Also, even changing that didn't get it to work.

      But

          $logger_factory = $this->createMock(LoggerChannelFactory::class);
          $logger_factory->expects($this->once())
            ->method('get')
            ->with('language')
            ->willReturn($logger);
          $this->container->set('logger.factory', $logger_factory);
      
      

      worked for me to fix the test.

    2. +++ b/core/modules/language/tests/src/Kernel/LanguageNegotiatorPluginTest.php
      @@ -0,0 +1,50 @@
      +    $this->assertFalse($logger->hasErrorThatContains('Valid plugin IDs for Drupal\language\LanguageNegotiationMethodManager are'));
      

      As @larowlan pointed out, this needs to be assertTrue.

      I'd change the string to 'The "Drupal\Tests\language\Kernel\LanguageNegotiatorPluginTest" plugin does not exist.' while we are there.

  • 🇪🇸Spain penyaskito Seville 💃, Spain 🇪🇸, UTC+2 🇪🇺
    +++ b/core/modules/language/tests/src/Kernel/LanguageNegotiatorPluginTest.php
    @@ -0,0 +1,50 @@
    +    $languageNegotiator->initializeType(LanguageInterface::TYPE_URL);
    +    $switchLinks = $languageManager->getLanguageSwitchLinks(LanguageInterface::TYPE_INTERFACE, new Url('<current>'));
    

    Let's also change the test to be explicit about the exception:

        try {
          $languageNegotiator->initializeType(LanguageInterface::TYPE_URL);
          $switchLinks = $languageManager->getLanguageSwitchLinks(LanguageInterface::TYPE_INTERFACE, new Url('<current>'));
        }
        catch(PluginNotFoundException $exception) {
          $this->fail('Plugin not found exception unhandled.');
        }
    
    
  • Status changed to Needs review over 1 year ago
  • 🇩🇪Germany sleitner

    Includes issues of #39-#42

  • Status changed to Needs work over 1 year ago
  • 🇪🇸Spain penyaskito Seville 💃, Spain 🇪🇸, UTC+2 🇪🇺
    1. +++ b/core/modules/language/tests/src/Kernel/LanguageNegotiatorPluginTest.php
      @@ -13,6 +16,7 @@
      +  use ProphecyTrait;
      
      @@ -24,27 +28,32 @@
      +    $languageNegotiator->setCurrentUser($this->getProphet()->prophesize('Drupal\Core\Session\AccountInterface')->reveal());
      

      We can call prophesize directly as you were doing. No need to include the trait.

    2. +++ b/core/modules/language/tests/src/Kernel/LanguageNegotiatorPluginTest.php
      @@ -24,27 +28,32 @@
      +      $switchLinks = $languageManager->getLanguageSwitchLinks(LanguageInterface::TYPE_INTERFACE, new Url('<current>'));
      

      We don't even need this line now that I see it, as it would fail on the initializeType method and we are not asserting on the switchLinks

  • 🇮🇳India Akram Khan Cuttack, Odisha

    address #44

  • 🇮🇳India Akram Khan Cuttack, Odisha

    try to fixed CCF #45

  • Status changed to Needs review over 1 year ago
  • 🇮🇳India Akram Khan Cuttack, Odisha
  • Status changed to RTBC over 1 year ago
  • 🇪🇸Spain penyaskito Seville 💃, Spain 🇪🇸, UTC+2 🇪🇺

    Looks great now, all feedback was addressed. Thanks!

  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    Updating issue credits

    • larowlan committed 8c76fe6d on 10.0.x
      Issue #3134349 by sleitner, yookoala, sharma.amitt16, Akram Khan, sahil....
    • larowlan committed b279c994 on 10.1.x
      Issue #3134349 by sleitner, yookoala, sharma.amitt16, Akram Khan, sahil....
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    removed these out of scope whitespace changes on commit

    diff --git a/core/modules/language/src/LanguageNegotiator.php b/core/modules/language/src/LanguageNegotiator.php
    index 72bf83cea9e..0007dab69ed 100644
    --- a/core/modules/language/src/LanguageNegotiator.php
    +++ b/core/modules/language/src/LanguageNegotiator.php
    @@ -340,13 +340,11 @@ public function updateConfiguration(array $types) {
             // settings are always considered non-configurable. In turn if default
             // settings are missing, the language type is always considered
             // configurable.
    -
             // If the language type is locked we can just store its default language
             // negotiation settings if it has some, since it is not configurable.
             if ($has_default_settings) {
               $method_weights = [];
               // Default settings are in $info['fixed'].
    -
               foreach ($info['fixed'] as $weight => $method_id) {
                 if (isset($method_definitions[$method_id])) {
                   $method_weights[$method_id] = $weight;

    Committed to 10.1.x and backported to 10.0.x

    Will backport to 9.5.x if test run passes

    • larowlan committed 05fb6a25 on 10.0.x
      Revert "Issue #3134349 by sleitner, yookoala, sharma.amitt16, Akram Khan...
  • Status changed to Needs work over 1 year ago
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    Reverted from 10.0.x as TestLogger does not exist there.

    You could possibly use BufferingLogger instead

    • larowlan committed 9fd7dd69 on 10.1.x
      Revert "Issue #3134349 by sleitner, yookoala, sharma.amitt16, Akram Khan...
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    Reverted from 10.1.x too, if we use BufferingLogger instead, we should have the same test in all three branches for consistency.

  • Status changed to RTBC over 1 year ago
  • 🇪🇸Spain penyaskito Seville 💃, Spain 🇪🇸, UTC+2 🇪🇺

    Took https://git.drupalcode.org/project/drupal/-/commit/b279c9946176bfc7a7dd5....
    Changed TestLogger for BufferingLogger. Took as a reference \Drupal\Tests\field\Kernel\EntityReference\EntityReferenceSettingsTest that uses that class, but kept the changes to the minimum.
    As I just did what @larowlan said, I think this is small enough to RTBC it myself.

    I've applied and run locally the test for 9.5.x, 10.0.x, 10.1.x, all green.

  • 🇪🇸Spain penyaskito Seville 💃, Spain 🇪🇸, UTC+2 🇪🇺

    One thing that I'm not sure how we operate with. There was a change record required at some point, and the draft was created. But it's not true anymore. Do we keep it as draft? Delete it? It can be confusing if we leave it as is.

    • larowlan committed d505351e on 10.0.x
      Issue #3134349 by sleitner, yookoala, sharma.amitt16, Akram Khan,...
    • larowlan committed 67fc8fbf on 10.1.x
      Issue #3134349 by sleitner, yookoala, sharma.amitt16, Akram Khan,...
    • larowlan committed faeae2b8 on 9.5.x
      Issue #3134349 by sleitner, yookoala, sharma.amitt16, Akram Khan,...
  • Status changed to Fixed over 1 year ago
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    Committed to 10.1.x and backported to 10.0.x and 9.5.x

    Deleted the change record

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

Production build 0.71.5 2024