Problem/Motivation

In multilingual Drupal sites where Project Browser results are translated or filtered based on the current language context, the QueryManager's internal caching mechanism causes a data collision.

The ProjectBrowserEndpointController::buildQuery() method creates a restricted $query array containing only a hardcoded list of parameters. Since identifying parameters (like language) are not included in this filtered array, the QueryManager generates identical cache keys for different languages, leading to stale or incorrectly localized data.

Steps to reproduce

  1. Set up a site with two languages (e.g., English and French).
  2. Implement a custom source plugin or alter the drupalorg_jsonapi source to provide translated descriptions. Or just use https://www.drupal.org/project/pb_localizer
  3. Browse projects in French.
  4. Switch the interface to English and browse the same projects.
  5. Observed behavior: The projects are still displayed in French because the QueryManager serves a cached response.

Proposed resolution

Modify ProjectBrowserEndpointController::buildQuery() to include all request query parameters in the $query array. This ensures that any parameter that might influence the source's output (like language or cache-busters) is correctly reflected in the QueryManager's cache key.

Proposed patch: 3589327-fix-multilingual-cache-collision.patch

expected result

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:

Comments

joachim namyslo created an issue. See original summary.

joachim namyslo’s picture

Issue summary: View changes
StatusFileSize
new550 bytes
joachim namyslo’s picture

Issue summary: View changes
StatusFileSize
new29.27 MB
joachim namyslo’s picture

Issue summary: View changes
joachim namyslo’s picture

Status: Active » Needs review
joachim namyslo’s picture

Issue summary: View changes
phenaproxima’s picture

I'm not sure how I feel about this. Could we maybe just append the current language (from LanguageManager) to the cache key instead?

phenaproxima’s picture

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

Also, wouldn't the language cache context (\Drupal\Core\Cache\Context\LanguagesCacheContext) come into play here?

I think we might want a test (kernel or functional) for this.

avpaderno’s picture

I apologize for this comment. I just want to remind that the report as spam link is not for comments somebody does not like; it is for real spam, where the comment advertises a product, a service, or a site, possibly with a link to that site, or to a page that describes that product or service.

joachim namyslo’s picture

I am not shure if deleting cache Key is enough. I didn't test that, yet. But this patch can be applied if you like to test it. I didn't see any side effects here since four weeks. But I am just one user.

joachim namyslo’s picture

joachim namyslo’s picture

added some files here. May be this is to much. how can we get that to a mr or fork?

Multilingual query cache collision — test coverage & verification report

Issue #3589327 · Project Browser · prepared 2026-06-13

Scope of verification. Both currently supported Project Browser lines, each in a real site:
  • Drupal 10.6.10 + Project Browser 2.0.2 · PHPUnit 9.6.34 · PHP 8.4.21
  • Drupal 11.3.11 + Project Browser 2.1.4 · PHPUnit 11.5.55 · PHP 8.4.21

1. Executive summary

This report responds to the Needs tests tag and to phenaproxima’s request for “a test (kernel or functional)”. It contains:

  • A kernel regression test (MultilingualCacheCollisionTest) that reproduces the cache-key collision and pins the corrected behaviour.
  • Empirical proof that the test fails on unpatched HEAD and passes with the fix, on both supported branches.
  • A combined patch (controller fix + the new test) that applies cleanly to PB 2.0.2 and 2.1.4 via both git apply and GNU patch.
Key design choice. The test asserts the observable contract, not a specific implementation. It therefore validates either resolution being discussed in the issue — copying the surviving request parameters into the query (the controller patch), or folding the active language into the cache key in QueryManager (phenaproxima’s suggestion). The test can land now; the implementation can be swapped later without touching it.

2. Root cause

The projects endpoint builds its query in ProjectBrowserEndpointController::buildQuery(). On unpatched HEAD that method copies only a fixed whitelist of request parameters (page, limit, machine_name, sort, search, categories, maintenance_status, development_status, security_advisory_coverage, source) into the array passed to QueryManager::getProjects().

That array is the sole input to the cache key:

// QueryManager::getQueryCacheKey()
return 'query:' . md5(Json::encode($query) . $lock_file_hash);

There is no language dimension anywhere in the key — not in the controller, not in QueryManager. Any request parameter outside the whitelist is discarded before it can influence the key. Consequently two requests that differ only by a language-distinguishing parameter resolve to an identical query:… cache id, and the first-cached language is served to every language. That is the user-visible defect reported in this issue.

3. The fix under test

The one-line controller change preserves the remaining request parameters so a distinguishing parameter can reach the cache key:

+    // Ensure all query parameters are passed to the QueryManager to allow
+    // for unique cache keys (e.g. for language or cache-busting).
+    $query += $request->query->all();
+
     return $query;

Re: the open review questions

phenaproxima asked whether the language should instead be appended to the cache key via LanguageManager, and whether the language cache context could be leveraged. Two points for the discussion:

  • The test does not presuppose the answer. It checks that a distinguishing parameter changes the key and that identical requests still share one. A LanguageManager-keyed implementation satisfies exactly the same assertions, so the suite is safe to commit ahead of that decision.
  • On cache contexts: QueryManager caches via a manually computed cid (an md5() over the query), not via render-array #cache metadata, so Drupal’s languages:language_interface context is not automatically consulted here. The closest core-idiomatic variant is to fold \Drupal::languageManager()->getCurrentLanguage()->getId() into getQueryCacheKey(). If the maintainers prefer that location, the same test stands and we are happy to supply that implementation.

4. Test design

  • Type: kernel test (KernelTestBase), as requested. Reuses the existing project_browser_test module’s project_browser_test_mock source and the real cache.project_browser bin — the same building blocks as the existing QueryManagerTest.
  • Behavioural, no reflection. It drives the real ProjectBrowserEndpointController::getAllProjects() with two HTTP requests and records what the QueryManager writes to the cache via a recording cache double. No private methods are touched, so the test stays valid if buildQuery() is refactored.
  • Two methods: a positive assertion (a distinguishing parameter yields a distinct key) and a control/negative assertion (two genuinely identical requests still share a key — guarding against over-keying that would defeat the cache entirely).
  • Cross-version metadata: both PHPUnit annotations and attributes are present, including #[RunTestsInSeparateProcesses] required from Drupal 11.3, so the file runs unchanged under PHPUnit 9.6 (D10) and 11.5 (D11).

5. Results — the proof

The same test was executed against each site with the patch applied and with the patch reversed (simulating unpatched HEAD).

Environment Patch state testDistinguishingParameterYieldsDistinctCacheKey testIdenticalRequestsShareCacheKey PHPUnit summary
Drupal 10.6.10 · PB 2.0.2 · PHPUnit 9.6 patched PASS PASS OK (2 tests, 4 assertions)
Drupal 10.6.10 · PB 2.0.2 · PHPUnit 9.6 unpatched FAIL PASS Tests: 2, Assertions: 4, Failures: 1
Drupal 11.3.11 · PB 2.1.4 · PHPUnit 11.5 patched PASS PASS OK (2 tests, 4 assertions)
Drupal 11.3.11 · PB 2.1.4 · PHPUnit 11.5 unpatched FAIL PASS Tests: 2, Assertions: 4, Failures: 1

On unpatched HEAD the failure is precisely the collision:

✘ Distinguishing parameter yields distinct cache key
  A request parameter that distinguishes the response (here: language) must change
  the cache key. Identical keys cause the multilingual cache collision reported in #3589327.
  Failed asserting that two strings are not identical.

The control test (testIdenticalRequestsShareCacheKey) passes in all states, confirming the failure is specific to the collision and not an artefact of the harness.

6. How to reproduce

# From the Drupal root, with drupal/core-dev installed and SIMPLETEST_DB exported:
php vendor/bin/phpunit -c $(pwd)/web/core/phpunit.xml.dist \
  web/modules/contrib/project_browser/tests/src/Kernel/MultilingualCacheCollisionTest.php --testdox

# To see it fail on unpatched HEAD, reverse the controller change first:
cd web/modules/contrib/project_browser
patch -p1 -R < /path/to/3589327-fix-multilingual-cache-collision.patch
# ...run phpunit again (collision test fails), then re-apply.

Note: under PHPUnit 9.6, pass an absolute -c path — process isolation (#[RunTestsInSeparateProcesses]) launches a child that cannot resolve a relative config path.

7. Patch applicability

The combined patch (controller fix + new test) was verified against a reconstructed pristine tree of each version, using both appliers cweagans/composer-patches v2 uses:

Project Browser git apply --check patch -p1 --fuzz=0
2.0.2 (Drupal 10) clean clean
2.1.4 (Drupal 11) clean clean

8. Attachments

  • 3589327-multilingual-cache-collision-with-test.patch — recommended combined patch: the controller fix and the kernel test.
  • 3589327-fix-multilingual-cache-collision.patch — original controller-only patch.
  • MultilingualCacheCollisionTest.php — the standalone test file (tests/src/Kernel/).
  • test-report.html — this report.

9. Recommendation

The behaviour is now covered by a passing, fail-on-regression kernel test on both supported branches. We recommend landing the combined patch. If the maintainers prefer to key the cache on the active language inside QueryManager::getQueryCacheKey() rather than widening the controller query, the included test already encodes the desired contract and will validate that implementation unchanged — we’re glad to provide it as an alternative MR.

Appendix A — combined patch

diff --git a/src/Controller/ProjectBrowserEndpointController.php b/src/Controller/ProjectBrowserEndpointController.php
index 0388d1a..5233f6b 100644
--- a/src/Controller/ProjectBrowserEndpointController.php
+++ b/src/Controller/ProjectBrowserEndpointController.php
@@ -178,6 +178,10 @@ final class ProjectBrowserEndpointController extends ControllerBase {
       $query['source'] = $displayed_source;
     }
 
+    // Ensure all query parameters are passed to the QueryManager to allow
+    // for unique cache keys (e.g. for language or cache-busting).
+    $query += $request->query->all();
+
     return $query;
   }
 
diff --git a/tests/src/Kernel/MultilingualCacheCollisionTest.php b/tests/src/Kernel/MultilingualCacheCollisionTest.php
new file mode 100644
index 0000000..a3fc787
--- /dev/null
+++ b/tests/src/Kernel/MultilingualCacheCollisionTest.php
@@ -0,0 +1,171 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\project_browser\Kernel;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\project_browser\ActivationManager;
+use Drupal\project_browser\Controller\ProjectBrowserEndpointController;
+use Drupal\project_browser\InstallProgress;
+use Drupal\project_browser\Plugin\ProjectBrowserSourceManager;
+use Drupal\project_browser\ProjectBrowser\Normalizer;
+use Drupal\project_browser\ProjectRepository;
+use Drupal\project_browser\QueryManager;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Tests that the projects endpoint produces language-distinct cache keys.
+ *
+ * Regression test for the multilingual cache collision described in
+ * https://www.drupal.org/project/project_browser/issues/3589327.
+ *
+ * The defect: ProjectBrowserEndpointController::buildQuery() copies only a fixed
+ * whitelist of request parameters into the array handed to
+ * QueryManager::getProjects(). QueryManager derives its cache key purely from
+ * that array (see QueryManager::getQueryCacheKey()). Therefore any request
+ * parameter outside the whitelist — including one a multilingual consumer adds
+ * to distinguish languages — is dropped before it can influence the cache key.
+ * Two requests that differ only by such a parameter then collide on a single
+ * cache entry, so the first-cached language is served to every language.
+ *
+ * This test pins the *observable contract* rather than a specific
+ * implementation: two endpoint requests that differ only by a distinguishing
+ * query parameter must not share a cache key. It therefore passes for either
+ * proposed fix — copying the remaining query parameters into the query (the
+ * controller-level fix), or folding the active language / a cache context into
+ * the cache key (the QueryManager-level fix) — and fails on unpatched HEAD.
+ *
+ * @group project_browser
+ * @coversClass \Drupal\project_browser\Controller\ProjectBrowserEndpointController
+ */
+#[CoversClass(ProjectBrowserEndpointController::class)]
+#[Group('project_browser')]
+#[RunTestsInSeparateProcesses]
+final class MultilingualCacheCollisionTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'project_browser',
+    'project_browser_test',
+    'user',
+  ];
+
+  /**
+   * Two requests differing only by a distinguishing param need distinct keys.
+   */
+  public function testDistinguishingParameterYieldsDistinctCacheKey(): void {
+    $this->config('project_browser.admin_settings')
+      ->set('enabled_sources', ['project_browser_test_mock' => []])
+      ->save();
+
+    // Record every cache id the QueryManager writes. Returning FALSE from get()
+    // forces a fresh query (and therefore a write) on each call, so the ids we
+    // collect are exactly the keys the endpoint computed for each request.
+    $written_cids = [];
+    $cache = $this->createMock(CacheBackendInterface::class);
+    $cache->method('get')->willReturn(FALSE);
+    $cache->method('set')
+      ->willReturnCallback(function (string $cid) use (&$written_cids): void {
+        $written_cids[] = $cid;
+      });
+    // Replace the bin before QueryManager is instantiated so it receives it.
+    $this->container->set('cache.project_browser', $cache);
+
+    $controller = $this->createEndpointController();
+
+    // Same source, page and limit; the requests differ only by the parameter a
+    // multilingual consumer (e.g. pb_localizer) appends to keep languages
+    // apart. In core today this parameter is dropped by buildQuery().
+    $controller->getAllProjects($this->endpointRequest(['langcode' => 'de']));
+    $controller->getAllProjects($this->endpointRequest(['langcode' => 'en']));
+
+    $this->assertCount(2, $written_cids, 'The endpoint queried (and cached) once per request.');
+    $this->assertNotSame(
+      $written_cids[0],
+      $written_cids[1],
+      'A request parameter that distinguishes the response (here: language) must change the cache key. Identical keys cause the multilingual cache collision reported in #3589327.',
+    );
+  }
+
+  /**
+   * Two genuinely identical requests must still share one cache key.
+   *
+   * Guards the fix against the opposite failure mode — over-keying the cache so
+   * that legitimately identical requests never hit it.
+   */
+  public function testIdenticalRequestsShareCacheKey(): void {
+    $this->config('project_browser.admin_settings')
+      ->set('enabled_sources', ['project_browser_test_mock' => []])
+      ->save();
+
+    $written_cids = [];
+    $cache = $this->createMock(CacheBackendInterface::class);
+    $cache->method('get')->willReturn(FALSE);
+    $cache->method('set')
+      ->willReturnCallback(function (string $cid) use (&$written_cids): void {
+        $written_cids[] = $cid;
+      });
+    $this->container->set('cache.project_browser', $cache);
+
+    $controller = $this->createEndpointController();
+    $controller->getAllProjects($this->endpointRequest(['langcode' => 'de']));
+    $controller->getAllProjects($this->endpointRequest(['langcode' => 'de']));
+
+    $this->assertCount(2, $written_cids);
+    $this->assertSame(
+      $written_cids[0],
+      $written_cids[1],
+      'Identical requests must resolve to the same cache key.',
+    );
+  }
+
+  /**
+   * Builds a GET request to the projects endpoint with extra query parameters.
+   *
+   * @param array $extra
+   *   Additional query parameters merged on top of a valid baseline request.
+   *
+   * @return \Symfony\Component\HttpFoundation\Request
+   *   The request.
+   */
+  private function endpointRequest(array $extra = []): Request {
+    return Request::create('/admin/modules/browse/project_browser_test_mock', 'GET', [
+      'source' => 'project_browser_test_mock',
+      'page' => 0,
+      'limit' => 12,
+    ] + $extra);
+  }
+
+  /**
+   * Instantiates the endpoint controller from container services.
+   *
+   * The controller is not registered as a service (it is autowired at routing
+   * time), so we assemble it from its dependencies. Its constructor signature is
+   * identical across the 2.0.x and 2.1.x lines this test targets.
+   *
+   * @return \Drupal\project_browser\Controller\ProjectBrowserEndpointController
+   *   The controller.
+   */
+  private function createEndpointController(): ProjectBrowserEndpointController {
+    $c = $this->container;
+    return new ProjectBrowserEndpointController(
+      $c->get(QueryManager::class),
+      $c->get(ProjectBrowserSourceManager::class),
+      $c->get(ProjectRepository::class),
+      $c->get(Normalizer::class),
+      $c->get('module_installer'),
+      $c->get('extension.list.module'),
+      $c->get(ActivationManager::class),
+      $c->get(InstallProgress::class),
+      $c->get('logger.channel.project_browser'),
+    );
+  }
+
+}

Appendix B — test source

<?php

declare(strict_types=1);

namespace Drupal\Tests\project_browser\Kernel;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\project_browser\ActivationManager;
use Drupal\project_browser\Controller\ProjectBrowserEndpointController;
use Drupal\project_browser\InstallProgress;
use Drupal\project_browser\Plugin\ProjectBrowserSourceManager;
use Drupal\project_browser\ProjectBrowser\Normalizer;
use Drupal\project_browser\ProjectRepository;
use Drupal\project_browser\QueryManager;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
use Symfony\Component\HttpFoundation\Request;

/**
 * Tests that the projects endpoint produces language-distinct cache keys.
 *
 * Regression test for the multilingual cache collision described in
 * https://www.drupal.org/project/project_browser/issues/3589327.
 *
 * The defect: ProjectBrowserEndpointController::buildQuery() copies only a fixed
 * whitelist of request parameters into the array handed to
 * QueryManager::getProjects(). QueryManager derives its cache key purely from
 * that array (see QueryManager::getQueryCacheKey()). Therefore any request
 * parameter outside the whitelist — including one a multilingual consumer adds
 * to distinguish languages — is dropped before it can influence the cache key.
 * Two requests that differ only by such a parameter then collide on a single
 * cache entry, so the first-cached language is served to every language.
 *
 * This test pins the *observable contract* rather than a specific
 * implementation: two endpoint requests that differ only by a distinguishing
 * query parameter must not share a cache key. It therefore passes for either
 * proposed fix — copying the remaining query parameters into the query (the
 * controller-level fix), or folding the active language / a cache context into
 * the cache key (the QueryManager-level fix) — and fails on unpatched HEAD.
 *
 * @group project_browser
 * @coversClass \Drupal\project_browser\Controller\ProjectBrowserEndpointController
 */
#[CoversClass(ProjectBrowserEndpointController::class)]
#[Group('project_browser')]
#[RunTestsInSeparateProcesses]
final class MultilingualCacheCollisionTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'project_browser',
    'project_browser_test',
    'user',
  ];

  /**
   * Two requests differing only by a distinguishing param need distinct keys.
   */
  public function testDistinguishingParameterYieldsDistinctCacheKey(): void {
    $this->config('project_browser.admin_settings')
      ->set('enabled_sources', ['project_browser_test_mock' => []])
      ->save();

    // Record every cache id the QueryManager writes. Returning FALSE from get()
    // forces a fresh query (and therefore a write) on each call, so the ids we
    // collect are exactly the keys the endpoint computed for each request.
    $written_cids = [];
    $cache = $this->createMock(CacheBackendInterface::class);
    $cache->method('get')->willReturn(FALSE);
    $cache->method('set')
      ->willReturnCallback(function (string $cid) use (&$written_cids): void {
        $written_cids[] = $cid;
      });
    // Replace the bin before QueryManager is instantiated so it receives it.
    $this->container->set('cache.project_browser', $cache);

    $controller = $this->createEndpointController();

    // Same source, page and limit; the requests differ only by the parameter a
    // multilingual consumer (e.g. pb_localizer) appends to keep languages
    // apart. In core today this parameter is dropped by buildQuery().
    $controller->getAllProjects($this->endpointRequest(['langcode' => 'de']));
    $controller->getAllProjects($this->endpointRequest(['langcode' => 'en']));

    $this->assertCount(2, $written_cids, 'The endpoint queried (and cached) once per request.');
    $this->assertNotSame(
      $written_cids[0],
      $written_cids[1],
      'A request parameter that distinguishes the response (here: language) must change the cache key. Identical keys cause the multilingual cache collision reported in #3589327.',
    );
  }

  /**
   * Two genuinely identical requests must still share one cache key.
   *
   * Guards the fix against the opposite failure mode — over-keying the cache so
   * that legitimately identical requests never hit it.
   */
  public function testIdenticalRequestsShareCacheKey(): void {
    $this->config('project_browser.admin_settings')
      ->set('enabled_sources', ['project_browser_test_mock' => []])
      ->save();

    $written_cids = [];
    $cache = $this->createMock(CacheBackendInterface::class);
    $cache->method('get')->willReturn(FALSE);
    $cache->method('set')
      ->willReturnCallback(function (string $cid) use (&$written_cids): void {
        $written_cids[] = $cid;
      });
    $this->container->set('cache.project_browser', $cache);

    $controller = $this->createEndpointController();
    $controller->getAllProjects($this->endpointRequest(['langcode' => 'de']));
    $controller->getAllProjects($this->endpointRequest(['langcode' => 'de']));

    $this->assertCount(2, $written_cids);
    $this->assertSame(
      $written_cids[0],
      $written_cids[1],
      'Identical requests must resolve to the same cache key.',
    );
  }

  /**
   * Builds a GET request to the projects endpoint with extra query parameters.
   *
   * @param array $extra
   *   Additional query parameters merged on top of a valid baseline request.
   *
   * @return \Symfony\Component\HttpFoundation\Request
   *   The request.
   */
  private function endpointRequest(array $extra = []): Request {
    return Request::create('/admin/modules/browse/project_browser_test_mock', 'GET', [
      'source' => 'project_browser_test_mock',
      'page' => 0,
      'limit' => 12,
    ] + $extra);
  }

  /**
   * Instantiates the endpoint controller from container services.
   *
   * The controller is not registered as a service (it is autowired at routing
   * time), so we assemble it from its dependencies. Its constructor signature is
   * identical across the 2.0.x and 2.1.x lines this test targets.
   *
   * @return \Drupal\project_browser\Controller\ProjectBrowserEndpointController
   *   The controller.
   */
  private function createEndpointController(): ProjectBrowserEndpointController {
    $c = $this->container;
    return new ProjectBrowserEndpointController(
      $c->get(QueryManager::class),
      $c->get(ProjectBrowserSourceManager::class),
      $c->get(ProjectRepository::class),
      $c->get(Normalizer::class),
      $c->get('module_installer'),
      $c->get('extension.list.module'),
      $c->get(ActivationManager::class),
      $c->get(InstallProgress::class),
      $c->get('logger.channel.project_browser'),
    );
  }

}


Generated for issue #3589327. Verified on Drupal 10.6.10 / Project Browser 2.0.2 (PHPUnit 9.6.34) and Drupal 11.3.11 / Project Browser 2.1.4 (PHPUnit 11.5.55), PHP 8.4.21.

joachim namyslo’s picture

Status: Needs work » Needs review

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

chrisfromredfin’s picture

MR !888 is taken from the patch (with test) from #12

joachim namyslo’s picture

StatusFileSize
new7.37 KB

Push a follow-up to fix the CI failure on MR !888, please

What was failing

Pipeline for 7386da21 (https://git.drupalcode.org/project/project_browser/-/merge_requests/888/...) had two red jobs:

  1. cspell (validate stage) — the actual blocker for this patch.
  2. phpunit: [FunctionalJavascript] — unrelated, see below.

The jobs that matter for this change, phpunit: [Kernel] and phpunit: [Unit], were already green, so MultilingualCacheCollisionTest itself was passing correctly with the fix.

Root cause of the cspell failure

cspell flagged the word "whitelist" twice in the new test's class docblock:

  • tests/src/Kernel/MultilingualCacheCollisionTest.php:28:4 - Forbidden word (whitelist)
  • tests/src/Kernel/MultilingualCacheCollisionTest.php:31:26 - Forbidden word (whitelist)

gitlab_templates' default .cspell.json forbids "whitelist"/"blacklist" as part of Drupal's inclusive-language policy and suggests "allowlist" instead. This was purely a docblock wording issue — the term described ProjectBrowserEndpointController::buildQuery()'s fixed set of copied request parameters, not any code identifier, so nothing else needed to change.

Fix

Replaced "whitelist" with "allowlist" in both docblock occurrences in MultilingualCacheCollisionTest.php (lines 28 and 31). No behavioral change, no code outside the comment touched. Updated patch is attached: 3589327-multilingual-cache-collision-with-test.patch (controller fix + regression test, cspell-clean).

About the other red job

phpunit: [FunctionalJavascript] failed with:

WebDriver\Exception\ElementClickIntercepted: element click intercepted: Element ... is not clickable at point (523, 212). Other element would receive the click ...

in ProjectBrowserPluginTest::testAdvancedFiltering (clicking the "Clear filters" button). This test doesn't touch language handling, caching, or buildQuery() at all, and the patch here doesn't change any markup or client-side behavior that this test interacts with. This looks like pre-existing flakiness in the WebDriver test (a timing/overlay issue independent of this change) rather than a regression introduced by this patch. Worth a job re-run to confirm; happy to file a separate issue if it reproduces consistently on unpatched HEAD.

chrisfromredfin’s picture

Status: Needs review » Reviewed & tested by the community

4 green checkmarks here, and a test.

joachim namyslo’s picture

Status: Reviewed & tested by the community » Closed (works as designed)

I’ve delved even deeper into the issue and added a new controller to the ProjectBrowser Localizer module, which makes this patch unnecessary because it replaces the one in ProjectBrowser—which simply isn’t designed to support multiple languages.

The advantage of this is that we don’t have to modify the ProjectBrowser code in this regard, and the translations of the project descriptions are available in multiple languages immediately after installing the module. We can therefore close this issue—the problem has been resolved directly within the module.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

chrisfromredfin’s picture

Well, that's a pleasant find! And, many thanks for your work in this area.