If you're running a multilingual Laravel site with mcamara/laravel-localization and spatie/laravel-responsecache, your response cache might be serving the wrong locale to visitors. Here's how we diagnosed and fixed a bug where our English site kept getting cached in Chinese.
The Problem: Laravel Multilingual Site Cache Stuck on Wrong Language
About once a day, our production Laravel 12 site started serving Chinese content to all visitors. The homepage, blog posts, service pages - everything rendered in Simplified Chinese instead of English. The only fix was clearing the response cache manually:
php artisan responsecache:clear
Content would go back to English... until the next day when it flipped to Chinese again.
Our Multilingual Laravel Stack
-
Laravel 12 with
mcamara/laravel-localizationfor URL-prefixed i18n (/en/blog,/zh/blog,/de/blog) -
spatie/laravel-responsecachefor full-page HTML caching (7-day TTL) usingDefaultHasher - Six supported locales including
zh(Chinese Simplified) -
hideDefaultLocaleInURLset tofalse-- all URLs have an explicit locale prefix
Root Cause: Why spatie/laravel-responsecache Ignores the Locale
The response cache stores rendered HTML keyed by a hash of the request URL. Since our URLs include locale prefixes (/en/blog vs /zh/blog), each locale should get its own cache entry. So how was Chinese content leaking into English pages?
Three things were conspiring against us.
1. useAcceptLanguageHeader Overrides URL Locale During Route Resolution
In config/laravellocalization.php, there's this setting:
'useAcceptLanguageHeader' => true,
This tells mcamara/laravel-localization: "If you can't determine the locale from the URL, check the browser's Accept-Language header."
Sounds reasonable. But LaravelLocalization::setLocale() runs during route resolution on every request. When the URL segment doesn't contain a recognized locale (e.g., a request to /), it calls getCurrentLocale() as a fallback:
public function getCurrentLocale()
{
if ($this->currentLocale) {
return $this->currentLocale;
}
if ($this->useAcceptLanguageHeader() && !$this->app->runningInConsole()) {
$negotiator = new LanguageNegotiator(...);
return $negotiator->negotiateLanguage();
}
return $this->configRepository->get('app.locale');
}
When a Chinese bot (Baidu, Sogou, etc.) hits the root URL / with Accept-Language: zh, the negotiator returns zh, and app()->setLocale('zh') is called as a side effect during route resolution. The page renders in Chinese, gets cached, and every subsequent visitor to that URL sees Chinese content until the cache expires or is cleared.
2. Spatie's DefaultHasher Doesn't Include Locale in the Cache Key
The DefaultHasher in spatie/laravel-responsecache generates cache keys like this:
return 'responsecache-' . hash('xxh128',
"{$request->getHost()}-{$request->getPathInfo()}-{$request->getMethod()}/{$suffix}"
);
The active locale is only present implicitly through the URL path. If any edge case causes app()->getLocale() to return the wrong locale during rendering, the wrong HTML gets cached under the "correct" URL's key. There's no defense-in-depth -- the DefaultHasher trusts that the URL path and the active locale always agree.
3. Synchronous Artisan::call() in Model Observers Mutates Locale State
Our ClearResponseCacheObserver did this on every model update :
protected function refreshSite(Model $model): void
{
ResponseCache::clear(); // Nuke the entire cache
Artisan::call('generate-sitemap'); // Runs synchronously!
$this->submitToIndexNow($model); // Notify search engines
}
The sitemap command calls LaravelLocalization::setLocale('en') internally, mutating the application locale during the active web request. Then submitToIndexNow notifies search engines (including Chinese ones) about all locale URLs via IndexNow, and their bots come crawling within seconds - while the response cache is still empty after the clear.
The Fix: Three Changes to Make Laravel Response Cache Locale-Aware
Fix 1: Custom Locale-Aware Hasher for spatie/laravel-responsecache
We created a custom hasher that extends DefaultHasher and explicitly includes app()->getLocale() in every cache key:
<?php
declare(strict_types=1);
namespace App\Http\CacheProfiles;
use Illuminate\Http\Request;
use Spatie\ResponseCache\Hasher\DefaultHasher;
class LocaleAwareHasher extends DefaultHasher
{
public function getHashFor(Request $request): string
{
$cacheNameSuffix = $this->getCacheNameSuffix($request);
$locale = app()->getLocale();
return 'responsecache-'.hash(
'xxh128',
"{$request->getHost()}-{$this->getNormalizedRequestUri($request)}-{$request->getMethod()}-{$locale}/{$cacheNameSuffix}"
);
}
}
Register it in config/responsecache.php:
'hasher' => \App\Http\CacheProfiles\LocaleAwareHasher::class,
Now even if the URL says /en/blog but app()->getLocale() somehow returns zh, they get separate cache entries instead of one overwriting the other. This makes locale cache contamination structurally impossible.
Fix 2: Disable useAcceptLanguageHeader in mcamara/laravel-localization
// config/laravellocalization.php
'useAcceptLanguageHeader' => false,
Since all our URLs have explicit locale prefixes (/en/, /zh/, etc.), there's no need for Accept-Language header detection. With this disabled, requests without a locale prefix fall back to the default app locale (en) instead of negotiating from whatever language a bot's headers specify.
This was the core trigger. One config line that let Chinese bots poison the response cache through the Accept-Language header.
Fix 3: Move Sitemap Generation to a Queued Job
We replaced the synchronous Artisan::call('generate-sitemap') in the model observer with a queued job:
// app/Jobs/GenerateSitemapJob.php
class GenerateSitemapJob implements ShouldQueue
{
public function handle(): void
{
Artisan::call('generate-sitemap');
}
}
// In the observer:
protected function refreshSite(Model $model): void
{
ResponseCache::clear();
GenerateSitemapJob::dispatch(); // Runs in isolated queue worker
$this->submitToIndexNow($model);
}
The sitemap command's LaravelLocalization::setLocale('en') call now runs in an isolated queue worker process.
How to Check If Your Laravel App Is Affected
You're likely vulnerable to this response cache locale bug if:
-
useAcceptLanguageHeaderistrueinconfig/laravellocalization.php - You're using
DefaultHasher(the default) inconfig/responsecache.php - You have non-Latin locales enabled (Chinese, Arabic, etc.) that make the bug immediately visible
- You're using
spatie/laravel-responsecachewithmcamara/laravel-localizationtogether
Don't Forget: Clear Response Cache After Deploy
Since the cache key format changes with the new LocaleAwareHasher, you must clear the existing response cache after deploying:
php artisan responsecache:clear
Top comments (0)