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
- Set up a site with two languages (e.g., English and French).
- Implement a custom source plugin or alter the
drupalorg_jsonapisource to provide translated descriptions. Or just use https://www.drupal.org/project/pb_localizer - Browse projects in French.
- Switch the interface to English and browse the same projects.
- Observed behavior: The projects are still displayed in French because the
QueryManagerserves 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
| Comment | File | Size | Author |
|---|---|---|---|
| #17 | 3589327-multilingual-cache-collision-with-test-v2.patch | 7.37 KB | joachim namyslo |
Issue fork project_browser-3589327
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
Comment #2
joachim namysloComment #3
joachim namysloComment #4
joachim namysloComment #5
joachim namysloComment #6
joachim namysloComment #7
phenaproximaI'm not sure how I feel about this. Could we maybe just append the current language (from
LanguageManager) to the cache key instead?Comment #8
phenaproximaAlso, 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.
Comment #9
avpadernoI 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.
Comment #10
joachim namysloI 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.
Comment #11
joachim namysloyou can find a more up to dete patch file here:
https://git.drupalcode.org/project/pb_localizer/-/blob/2.0.x/patches/358...
Comment #12
joachim namysloadded some files here. May be this is to much. how can we get that to a mr or fork?
Issue #3589327 · Project Browser · prepared 2026-06-13
1. Executive summary
This report responds to the Needs tests tag and to phenaproxima’s request for “a test (kernel or functional)”. It contains:
MultilingualCacheCollisionTest) that reproduces the cache-key collision and pins the corrected behaviour.git applyand GNUpatch.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 toQueryManager::getProjects().That array is the sole input to the cache key:
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 identicalquery:…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:
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:LanguageManager-keyed implementation satisfies exactly the same assertions, so the suite is safe to commit ahead of that decision.QueryManagercaches via a manually computedcid(anmd5()over the query), not via render-array#cachemetadata, so Drupal’slanguages:language_interfacecontext is not automatically consulted here. The closest core-idiomatic variant is to fold\Drupal::languageManager()->getCurrentLanguage()->getId()intogetQueryCacheKey(). If the maintainers prefer that location, the same test stands and we are happy to supply that implementation.4. Test design
KernelTestBase), as requested. Reuses the existingproject_browser_testmodule’sproject_browser_test_mocksource and the realcache.project_browserbin — the same building blocks as the existingQueryManagerTest.ProjectBrowserEndpointController::getAllProjects()with two HTTP requests and records what theQueryManagerwrites to the cache via a recording cache double. No private methods are touched, so the test stays valid ifbuildQuery()is refactored.#[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).
testDistinguishingParameterYieldsDistinctCacheKeytestIdenticalRequestsShareCacheKeyOn unpatched HEAD the failure is precisely the collision:
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
Note: under PHPUnit 9.6, pass an absolute
-cpath — 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-patchesv2 uses:git apply --checkpatch -p1 --fuzz=08. 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
Appendix B — test source
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.
Comment #13
joachim namysloComment #16
chrisfromredfinMR !888 is taken from the patch (with test) from #12
Comment #17
joachim namysloPush 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:
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:
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.
Comment #18
chrisfromredfin4 green checkmarks here, and a test.
Comment #19
joachim namysloI’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.
Comment #21
chrisfromredfinWell, that's a pleasant find! And, many thanks for your work in this area.