DEV Community

Cover image for Laravel Response Cache Serving Wrong Language: Fixing spatie/laravel-responsecache with mcamara/laravel-localization
Dawid Makowski
Dawid Makowski

Posted on

Laravel Response Cache Serving Wrong Language: Fixing spatie/laravel-responsecache with mcamara/laravel-localization

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
Enter fullscreen mode Exit fullscreen mode

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-localization for URL-prefixed i18n (/en/blog, /zh/blog, /de/blog)
  • spatie/laravel-responsecache for full-page HTML caching (7-day TTL) using DefaultHasher
  • Six supported locales including zh (Chinese Simplified)
  • hideDefaultLocaleInURL set to false -- 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,
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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}"
);
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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}"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it in config/responsecache.php:

'hasher' => \App\Http\CacheProfiles\LocaleAwareHasher::class,
Enter fullscreen mode Exit fullscreen mode

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,
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. useAcceptLanguageHeader is true in config/laravellocalization.php
  2. You're using DefaultHasher (the default) in config/responsecache.php
  3. You have non-Latin locales enabled (Chinese, Arabic, etc.) that make the bug immediately visible
  4. You're using spatie/laravel-responsecache with mcamara/laravel-localization together

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)