DEV Community

Cover image for Fixing Spatie's Laravel ResponseCache to Respect Accept-Language
Dawid Makowski
Dawid Makowski

Posted on

Fixing Spatie's Laravel ResponseCache to Respect Accept-Language

I’ll be honest. Few things are more frustrating than solving a problem by reaching for a great package… only to realize the problem is still there. That was me last week, yelling at my API like it had just spoiled the finale of a Netflix show.

I was using Spatie’s Laravel ResponseCache package. It’s rock solid, it works out of the box, and it’s built by people I trust. But here’s the kicker: I turned it on in production and suddenly my multilingual API was speaking one language only. The first request cached in English. Every Arabic request after that? Still English.

So here’s what it took to fix it. I hope it will help some lost soul on the internet one day.

Photo by Douglas Lopes on Unsplash


The Problem

Spatie’s ResponseCache builds cache keys from the host, normalized URI, HTTP method, and a suffix (usually the user ID). That works fine most of the time, but it completely ignores request headers like Accept-Language.

Which means:

  • First request with Accept-Language: en → response cached in English
  • Second request with Accept-Language: ar → still gets the English version

My API became linguistically challenged.


The Fix: Middleware That Cares About Language

Spatie lets you influence the cache key by setting a per-request attribute called responsecache.cacheNameSuffix. All we have to do is populate it with something that changes when the language changes.

Here’s the middleware I ended up shipping:

<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class SetResponseCacheSuffixFromLanguage
{
    public function handle(Request $request, Closure $next)
    {
        // Take the first language from the Accept-Language header
        $accept = Str::of($request->header('Accept-Language', ''))
            ->before(',')
            ->replace('_', '-')
            ->lower()
            ->value();

        // Add user id if you want to separate caches per user
        $userId = auth()->id();

        $suffix = implode('|', array_filter([$userId, $accept]));
        if ($suffix !== '') {
            $request->attributes->set('responsecache.cacheNameSuffix', $suffix);
        }

        // Optional: add a debug header so you can see what suffix is being used
        $response = $next($request);
        if ($suffix !== '') {
            $response->headers->set('X-ResponseCache-Suffix', $suffix);
        }

        return $response;
    }
}
Enter fullscreen mode Exit fullscreen mode

Registering the Middleware

In Laravel 12, add it globally in bootstrap/app.php:

use App\Http\Middleware\SetResponseCacheSuffixFromLanguage;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->append(SetResponseCacheSuffixFromLanguage::class);
    })
    ->create();
Enter fullscreen mode Exit fullscreen mode

Now the cache key varies by Accept-Language and you no longer serve Arabic users the English version of your API.


Adding the Vary Header

If you’re running behind AWS ALB or a CDN, make sure your responses include Vary: Accept-Language. Otherwise proxies and browsers might serve the wrong cached version.

Just drop this into the middleware:

$response->headers->set(
    'Vary',
    trim(($response->headers->get('Vary') ?: '') . ', Accept-Language', ', ')
);
Enter fullscreen mode Exit fullscreen mode

Don’t Forget to Clear Old Cache

The existing cache entries are language-agnostic, so clear them once:

php artisan responsecache:clear
Enter fullscreen mode Exit fullscreen mode

How to Prove It Works

Run these two curls and check the header:

curl -H 'Accept-Language: en' https://api.example.com/v1/pages -I | grep X-ResponseCache-Suffix
curl -H 'Accept-Language: ar' https://api.example.com/v1/pages -I | grep X-ResponseCache-Suffix
Enter fullscreen mode Exit fullscreen mode

You should see en vs ar. That’s your proof the cache is now language-aware.

Will be chasing Spatie's team to add this to the package as well, fingers crossed!


Check my personal blog for more tech-related content.

Top comments (1)

Collapse
 
xwero profile image
david duymelinck • Edited

I turned it on in production and suddenly my multilingual API was speaking one language only.

That seems to me like a bad way to test changes.

I was curious why your solution worked so I dug into the code and I found the working part in the DefaultHasher.

protected function getCacheNameSuffix(Request $request)
{
     if ($request->attributes->has('responsecache.cacheNameSuffix')) {
        return $request->attributes->get('responsecache.cacheNameSuffix');
     }

    return $this->cacheProfile->useCacheNameSuffix($request);
}
Enter fullscreen mode Exit fullscreen mode

In the readme you can see the hasher is configurable. So If you created your own hasher and added the accept language header to the getNormalizedRequestUri method you don't need the middleware solution.

My guess is that they are not going to change the package, because they already gave you two ways to make the package fit your application.