kevinrob/guzzle-cache-middleware
RFC 7234-compliant HTTP cache middleware for Guzzle 6+. Add to a HandlerStack to transparently cache responses and speed up API calls. Supports PSR-7 and multiple backends: Laravel Cache, Flysystem, PSR-6/16, and WordPress object cache.
Install the package:
composer require kevinrob/guzzle-cache-middleware
Basic Guzzle Client Integration:
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Kevinrob\GuzzleCache\CacheMiddleware;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\Psr6CacheStorage;
use Cache\Adapter\PHPArray\ArrayCachePool;
// Create a PSR-6 cache pool (in-memory for demo)
$cachePool = new ArrayCachePool();
// Initialize storage and strategy
$storage = new Psr6CacheStorage($cachePool);
$strategy = new PrivateCacheStrategy($storage);
// Create and configure Guzzle client
$stack = HandlerStack::create();
$stack->push(new CacheMiddleware($strategy), 'cache');
$client = new Client(['handler' => $stack]);
First Use Case:
// Cache will automatically handle GET requests to this URL
$response = $client->get('https://api.example.com/data');
// Subsequent identical requests will return cached response
$cachedResponse = $client->get('https://api.example.com/data');
PrivateCacheStrategy, PublicCacheStrategy, and GreedyCacheStrategy.Middleware Placement:
Always push the CacheMiddleware to the top of the HandlerStack to ensure it processes requests before other middleware (e.g., retries, logging).
$stack->push(new CacheMiddleware($strategy), 'cache');
Strategy Selection:
PrivateCacheStrategy for user-specific or sensitive data (e.g., authenticated API calls).PublicCacheStrategy for shared, non-sensitive data (e.g., public APIs).GreedyCacheStrategy for APIs with poor caching headers or when you need to enforce a TTL.Storage Integration:
Laravel developers should leverage LaravelCacheStorage for seamless integration with Laravel's cache drivers (Redis, Memcached, etc.):
$storage = new LaravelCacheStorage(Cache::store('redis'));
Dynamic TTL with Greedy Strategy: Override TTL per request using headers:
$request = $client->get('https://api.example.com/data', [
'headers' => [
GreedyCacheStrategy::HEADER_TTL => 3600, // 1 hour
],
]);
Assign different strategies based on the request domain or endpoint:
$delegateStrategy = new DelegatingCacheStrategy(new NullCacheStrategy());
$delegateStrategy->registerRequestMatcher(
new class implements RequestMatcherInterface {
public function matches(RequestInterface $request) {
return str_contains($request->getUri()->getHost(), 'api.example.com');
}
},
new PrivateCacheStrategy($storage)
);
Manually invalidate cached entries for specific requests:
$client->get('https://api.example.com/data', [
'cache' => [
'invalidate' => true, // Force cache invalidation
],
]);
Or globally clear the cache:
$storage->clear(); // For PSR-6/PSR-16 storage
Cache::forget('guzzle-cache-key'); // For Laravel storage
Disable caching for specific requests using the cache option:
$client->get('https://api.example.com/uncached-data', [
'cache' => [
'enabled' => false,
],
]);
Register the client in a Laravel service provider:
public function register()
{
$this->app->singleton('guzzle.cache.client', function ($app) {
$stack = HandlerStack::create();
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new LaravelCacheStorage(Cache::store('redis'))
)
),
'cache'
);
return new Client(['handler' => $stack]);
});
}
Mock the cache storage in tests:
$mockStorage = $this->createMock(Psr6CacheStorageInterface::class);
$mockStorage->method('fetch')->willReturn($cachedResponse);
$strategy = new PrivateCacheStrategy($mockStorage);
Cache Key Collisions:
Authorization) may generate the same cache key, leading to incorrect caching.GreedyCacheStrategy with KeyValueHttpHeader to include dynamic headers in the cache key:
new GreedyCacheStrategy($storage, 3600, new KeyValueHttpHeader(['Authorization']))
Binary Data Truncation:
LocalFilesystemAdapter). Use Psr6CacheStorage with a binary-safe cache pool like Cache\Adapter\Filesystem\FilesystemCachePool.TTL Misconfiguration:
0 or negative) can cause cache entries to expire immediately or behave unpredictably.GreedyCacheStrategy or use PrivateCacheStrategy/PublicCacheStrategy to rely on HTTP headers.Laravel Cache Driver Quirks:
file, database) may not handle binary data or large payloads well.redis or memcached for production. For file-based caching, use Flysystem:
new FlysystemStorage(new LocalFilesystemAdapter(storage_path('app/guzzle-cache')))
Stale-While-Revalidate:
stale-while-revalidate may not be cached correctly.allowStaleWhileRevalidate (most PSR-6 adapters do).Middleware Order:
CacheMiddleware after other middleware (e.g., retries, logging) may lead to inconsistent caching behavior.CacheMiddleware to the top of the stack:
$stack->push(new CacheMiddleware($strategy), 'cache');
DST and Timezone Issues:
PSR-20 Clock (supported since v8.0.0) for deterministic time management:
use Kevinrob\GuzzleCache\Clock\SystemClock;
$strategy = new PrivateCacheStrategy($storage, new SystemClock());
Inspect Cache Entries:
Use the inspectAll utility to debug cached entries:
use Kevinrob\GuzzleCache\Utils;
Utils::inspectAll($storage); // Lists all cached entries
Log Cache Hits/Misses:
Extend CacheMiddleware to log cache events:
class LoggingCacheMiddleware extends CacheMiddleware
{
public function __construct(StrategyInterface $strategy)
{
parent::__construct($strategy);
}
protected function handle(RequestInterface $request, $next)
{
$response = parent::handle($request, $next);
\Log::info('Cache ' . ($this->isCacheHit ? 'hit' : 'miss'), [
'request' => $request->getUri(),
'method' => $request->getMethod(),
]);
return $response;
}
}
Validate Cache Headers:
Ensure responses include proper caching headers (Cache-Control, ETag, Last-Modified). Use tools like HTTP Header Live Viewer to verify.
**Check for Corrupted Entries
How can I help you explore Laravel packages today?