slim/http-cache
A PSR-15 middleware that adds HTTP caching to Slim 4 apps, handling ETag/Last-Modified conditional requests, Cache-Control headers, and filesystem response caching to cut bandwidth and speed up repeat visits.
Install with Composer (there is no Laravel service provider auto-discovery — this is a PSR-7 middleware package):
composer require slim/http-cache
Where to look first
src/Cache.php — the middleware itself and the fluent helpers (withEtag(), withLastModified(), withCacheControl(), withExpires()). Read this file end-to-end; it is small and is the real API surface.src/CacheProvider.php — a trait that lazy-instantiates Cache and proxies the with* methods onto $this.Important compatibility check before you commit: the middleware uses the Slim 3 signature __invoke(RequestInterface $request, ResponseInterface $response, callable $next). If your service is on Slim 4 (PSR-15 process()), this class will not drop into the middleware stack as-is — you either need a small adapter or to call the with* helpers directly from your handlers. In a Laravel app this package is not a drop-in: Laravel's HTTP kernel does not speak PSR-7 natively, so treat it as something you use in Slim microservices/sidecars that sit next to a Laravel app, or as a reference implementation for a Laravel CacheHeaders middleware.
First use case: a read-only JSON API where you want ETag + Cache-Control on every response:
$app = new \Slim\App();
$app->add(new \Slim\HttpCache\Cache('public', 3600)); // Cache-Control: public, max-age=3600
That single line is the 80% case: it adds Cache-Control, Expires, and (if configured) validator headers to whatever response the route produced, and answers conditional requests with 304 Not Modified.
Pattern 1 — blanket headers for a whole API
$app->add(new \Slim\HttpCache\Cache('public', 86400));
Good for documentation endpoints and truly static lists. Bad for anything user-specific; prefer scoping it to a route group.
Pattern 2 — per-route validators via the CacheProvider trait
This is where the package earns its keep day-to-day. The trait gives you the with* helpers on a controller/handler:
use Slim\HttpCache\CacheProvider;
class ArticleController
{
use CacheProvider;
public function show($request, $response, $args)
{
$article = $this->articles->find($args['id']);
$etag = md5($article->id . ':' . $article->updated_at->timestamp);
$response = $this->withEtag($response, $etag); // strong ETag
$response = $this->withLastModified(
$response,
$article->updated_at->getTimestamp()
);
$response = $this->withCacheControl($response, 'public, max-age=60');
return $response->withJson($article);
}
}
The middleware then performs the comparison: an incoming If-None-Match matching your ETag, or If-Modified-Since >= your Last-Modified, results in a 304 with an empty body.
Pattern 3 — weak ETags for content-negotiated responses
Useful when the body differs by encoding but the underlying resource is the same:
$response = $this->withEtag($response, $etag, 'weak'); // emits W/"..."
Pattern 4 — private/no-store for authenticated resources
$response = $this->withCacheControl($response, 'private, no-store, max-age=0');
$response = $response->withHeader('Vary', 'Authorization');
The package does not add Vary for you. If your response varies by Accept, Accept-Encoding, or auth, you must add it yourself, otherwise downstream proxies will poison the cache.
Pattern 5 — mixing middleware and per-route helpers
Put new Cache(...) at the outermost layer (added last) so it sees the final headers your handler set. Keep validator computation inside the handler (withEtag / withLastModified) and let the middleware own the 304 decision. Do not have the middleware set an ETag and the route set one — you'll get a duplicated or overwritten header.
Integration tip for Laravel teams: if you run a Laravel app plus a Slim gateway (or are migrating), the cheapest integration is to have the Slim layer handle ETag/Last-Modified/Cache-Control and let Laravel's own Cache-Control middleware handle app responses. If you want the same behavior inside Laravel, port the ~150 lines of Cache::__invoke logic into an Illuminate\Http\Middleware style class — the validation rules (RFC 7232) are the valuable part, not the Slim plumbing.
Middleware order is the #1 bug source. Add the cache middleware last so it wraps the final response. If it runs before your controller, headers are missing and you never get a 304.
PSR-7 immutability. withEtag() returns a new response. $this->withEtag($response, $etag); without reassigning is a silent no-op. Always $response = $this->withEtag(...).
304 bodies. A 304 must not carry a body. Verify the middleware replaces the stream with an empty one; if you're on a Slim 4 adapter, this is exactly the edge case that gets missed. curl -i a conditional request and confirm Content-Length: 0 or no body.
ETag quoting. The helper emits quoted ETags per RFC ("abc"). If you paste a pre-quoted value you'll produce ""abc"" and clients will never match. Pass the raw hash.
Last-Modified has one-second granularity. Two updates in the same second look identical to caches. If you have high write rates, rely on ETag for correctness and treat Last-Modified as advisory.
Expires vs Cache-Control. When both are present, max-age wins in every modern client. Don't be surprised when tweaking Expires appears to do nothing — change the max-age argument.
no-cache ≠ no-store. no-cache still stores and revalidates. For sensitive payloads use no-store, private.
Don't hash large bodies for ETags. md5($response->getBody()) forces full materialization and breaks streaming. Hash a row version, updated_at, or a content digest computed at write time.
Debugging conditional requests:
curl -i -H 'If-None-Match: "your-etag"' https://api.test/articles/1
curl -i -H 'If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT' https://api.test/articles/1
A 200 where you expect 304 almost always means (a) the handler reassigned the response after setting headers, (b) a proxy stripped If-None-Match, or (c) the ETag changed between requests.
Extension points. Cache is a plain class — subclass it to inject Vary or standard security headers, or wrap it with your own middleware to log cache hit/miss decisions. The with* helpers are also usable directly outside the middleware if you only want to emit headers and handle 304s yourself.
Slim 4 / PSR-15. Write a thin adapter (process() → invoke the Slim-3-style callable) or skip the middleware and call the with* methods from handlers. Don't assume composer require makes it work in a Laravel or Slim 4 stack without that shim.
Pin the version. The package is small and low-traffic (116 stars, zero dependents); pin a known-good tag in composer.json and cover the 304 path with a functional test so a future minor bump can't silently change header semantics.
How can I help you explore Laravel packages today?