Install
$ agentstack add skill-lonsdale201-wp-agent-skills-br-etag-cache ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
better-route: ETag-based response caching
For developers adding HTTP-level cacheability to better-route GET / HEAD endpoints. The middleware emits an ETag header with a body hash, accepts If-None-Match from the client, and returns 304 Not Modified (no body) when they match — saving bandwidth and letting CDNs cache responses across users.
Misconception this skill corrects
> "I'll add ETagMiddleware to my POST endpoint so duplicate requests return 304."
Wrong tool. ETag is for read caching only. Verified at [src/Middleware/Cache/ETagMiddleware.php:28-30](ETagMiddleware.php) — non-GET / non-HEAD requests pass through unchanged. For "duplicate POSTs return cached response", use IdempotencyMiddleware (see br-idempotency) — different mechanism, different goal.
The middleware only applies to:
- HTTP method GET or HEAD ([line 53-61](ETagMiddleware.php)
isGetOrHead). - Response status 2xx but NOT 204 ([line 33-35](ETagMiddleware.php)) — 204 has no body, no ETag.
Other AI-prone misconceptions:
- "ETag is computed cheap, so I can add it everywhere." Half-true. The default ETag is
sha1(json_encode($body))— not free. For a 50KB JSON response per request, that's ~50µs per request just hashing. For high-traffic endpoints, pass anetagResolverthat reads a cheap source (last-modified timestamp). - "ETag response header alone enables caching." Half-true. ETag enables CONDITIONAL caching (revalidate via If-None-Match). For unconditional caching, also set
Cache-Controlheaders — the middleware doesn't do that. - "
weak: truemeans weak hashing." Wrong —weak: trueemits aW/"hash"validator (HTTP weak ETag) which allows semantic equivalence rather than byte equivalence. The hash itself is the same sha1 either way.
When to use this skill
Trigger when ANY of the following is true:
- The diff instantiates
ETagMiddleware. - A handler returns large JSON arrays that don't change often (catalog listings, public configs).
- Setting up CDN-friendly caching on a public REST endpoint.
- Reviewing a PR that hand-rolls
ETag/If-None-Matchlogic.
Workflow
1. Default usage (body hash)
use \BetterRoute\Middleware\Cache\ETagMiddleware;
$etag = new ETagMiddleware(); // weak = false, etagResolver = null
$router->get('/profile/{id}', $handler)
->middleware([$etag]);
The middleware:
- Calls
$next($context)— gets the handler's response. - Computes the ETag —
'"' . sha1(json_encode($body)) . '"'([line 70-77](ETagMiddleware.php)). - Compares against
If-None-Matchfrom the request. - If match: returns
new Response(null, 304, ['ETag' => $etag])— no body. - If miss: returns the response with the ETag header attached.
2. Custom etagResolver for cheap hashing
$etag = new ETagMiddleware(
weak: false,
etagResolver: function (mixed $response, RequestContext $context): string {
// Cheap hash from a known invariant — last-modified timestamp + ID
$body = $response instanceof Response ? $response->body : $response;
$id = $body['id'] ?? 0;
$modified = $body['modified'] ?? 0;
return "{$id}-{$modified}"; // the middleware quotes it: "5-1714356000"
},
);
Use when:
- The body is large (1MB+) and re-hashing on every request is expensive.
- You have a cheap source-of-truth for "did this change" (DB column, cache key, file mtime).
- The body content has unstable serialization order —
json_encodeof an associative array can hash differently on different PHP runs / versions.
The resolver receives ($response, $context) — return any string; the middleware wraps it in quotes (and W/ prefix if weak: true).
3. Weak validators
$etag = new ETagMiddleware(weak: true);
Emits W/"" instead of "". Per RFC 7232, weak validators allow semantic equivalence — two responses that are byte-different but semantically equivalent (e.g. JSON with reordered keys) can match.
In practice for JSON APIs, the difference is negligible — most CDNs treat both the same. Default weak: false is correct for byte-exact validation.
4. Combine with Cache-Control for true cacheability
ETag alone enables revalidation: client sends If-None-Match, server may return 304. To actually cache without revalidating each time, the response also needs Cache-Control:
$router->get('/catalog', function ($ctx) {
$body = expensive_fetch();
return new \BetterRoute\Http\Response(
body: $body,
status: 200,
headers: [
'Cache-Control' => 'public, max-age=300', // cache for 5 minutes
]
);
})->middleware([new ETagMiddleware()]);
Now the response is cached for 5 minutes by the client / CDN. After that, the client revalidates with If-None-Match — and the server returns 304 if the data hasn't changed. The cache extends until the data DOES change.
5. Group-level ETag
$router->group('/public', function ($group) {
$group->get('/featured', $featured);
$group->get('/categories', $categories);
$group->get('/posts', $posts);
})->middleware([new ETagMiddleware()]);
All three GETs get ETag treatment. Mid-request middleware decides whether to apply per-request based on method and status.
6. Skip ETag for specific routes in a group
$router->group('/public', function ($group) {
$group->get('/featured', $featured);
$group->get('/now', $serverTime); // changes every request — no cache benefit
})->middleware([new ETagMiddleware()]);
ETag still runs on /now, but since the body changes per request (new timestamp), the If-None-Match never matches. The middleware computes the hash and emits ETag, then 304 never fires. Slight overhead with no caching benefit.
To skip cleanly, attach the middleware per-route instead of per-group:
$router->get('/featured', $featured)->middleware([new ETagMiddleware()]);
$router->get('/now', $serverTime); // no ETag
Critical rules
- GET and HEAD only. Other methods pass through unchanged ([ETagMiddleware.php:28-30](ETagMiddleware.php)).
- 2xx-non-204 only. 304, 4xx, 5xx, 204 don't get an ETag header.
- Default ETag is
"sha1(json_encode($body))". Symmetric quoting (RFC 7232). - Weak validators (
weak: true) emitW/"hash". Allow semantic equivalence; rarely needed for JSON APIs. etagResolveris the cheap-hash escape hatch. Use for large bodies or stable invariants.- ETag is conditional caching. For unconditional caching, also set
Cache-Control. json_encodeorder can vary — for stable hashing on rotating-key data, sort the array first or use anetagResolverbased on a stable invariant.- The middleware respects existing
ETagheaders in the response (it doesn't overwrite if your handler set one already — verify on your specific case).
Common mistakes
// WRONG — ETag on a write endpoint
$router->post('/articles', $handler)
->middleware([new ETagMiddleware()]);
// ETagMiddleware passes POST through unchanged. No effect, dead code.
// RIGHT — ETag on read endpoints only
$router->get('/articles', $listHandler)->middleware([new ETagMiddleware()]);
// RIGHT — for write idempotency, use IdempotencyMiddleware
// WRONG — expecting ETag alone to cache the response
$router->get('/catalog', $heavyHandler)
->middleware([new ETagMiddleware()]);
// First request: handler runs, ETag emitted. Second request without If-None-Match: handler
// runs AGAIN. ETag enables revalidation, not caching.
// RIGHT — combine with Cache-Control
$router->get('/catalog', function ($ctx) {
return new Response(body: $data, headers: ['Cache-Control' => 'public, max-age=300']);
})->middleware([new ETagMiddleware()]);
// WRONG — default body-hash on a 5MB response
$router->get('/full-export', $hugeJsonHandler)
->middleware([new ETagMiddleware()]);
// Each request hashes 5MB → ~10ms hashing per request → noticeable on a high-RPS endpoint.
// RIGHT — etagResolver from a stable invariant
$router->get('/full-export', $hugeJsonHandler)
->middleware([new ETagMiddleware(
etagResolver: fn ($response, $ctx) => (string) get_option('myapp_export_version', 0),
)]);
// WRONG — assuming json_encode order is stable across PHP versions
// Same data hashes differently between PHP runs → ETag never matches → cache useless
// RIGHT — sort the data first OR use a stable invariant via etagResolver
function ($response) {
$body = $response->body;
ksort($body); // stable key order
return sha1(json_encode($body));
}
// WRONG — quoting the ETag manually
->headers(['ETag' => 'abc123'])
// HTTP requires ETag values to be quoted. The middleware adds quotes; you don't need to.
// RIGHT — return raw hash string, middleware quotes it
return $hash; // middleware emits ETag: "$hash"
// WRONG — using ETag for per-user cached responses without varying by user
$etag = new ETagMiddleware();
$router->get('/me', $myProfile)->middleware([$etag]);
// Two different users get the same /me URL. CDN caches user A's response and serves it to user B.
// RIGHT — for per-user responses, ALSO set 'Cache-Control: private' so CDNs don't share:
return new Response(body: $myProfile, headers: [
'Cache-Control' => 'private, max-age=60',
]);
// And ETag works correctly per user (each browser caches its own copy).
Cross-references
- Run
br-routesfor the route-layer middleware attachment patterns (->middleware([...]), group middleware). - Run
br-idempotencywhen the goal is "duplicate POST returns cached result" (different mechanism, write side). - Run
br-rate-limitingwhen combining ETag with rate limits — the order matters for the per-user-vs-IP key. - Run
br-error-contractfor how 304 differs from the error envelope (304 has no body; errors have{error: ...}).
What this skill does NOT cover
- Last-Modified / If-Modified-Since semantics. ETag and Last-Modified are separate validators; the middleware ships ETag only.
- Cache-Control header configuration. The handler / route is responsible for setting
max-age,private, etc. - CDN-specific cache invalidation (Cloudflare purge API, etc.). External infrastructure.
- Per-user cache key derivation. Use
etagResolverif you need per-user hashing. - Compression / Vary headers. Set
Vary: Accept-Encodingseparately if needed. - Browser cache flushing. Client-side concern.
References
- ETagMiddleware: [libraries/better-route/src/Middleware/Cache/ETagMiddleware.php:11](ETagMiddleware.php) —
final class ETagMiddleware implements MiddlewareInterface. Constructor at line 18-23 (weak: false,etagResolver: nulldefaults). - GET/HEAD guard: [ETagMiddleware.php:28-30](ETagMiddleware.php) —
isGetOrHeadcheck. - 2xx-non-204 guard: [ETagMiddleware.php:33-35](ETagMiddleware.php) — status range check.
- 304 short-circuit: [ETagMiddleware.php:38-40](ETagMiddleware.php) —
If-None-Matchcomparison + 304 response. - Default hash: [ETagMiddleware.php:70-77](ETagMiddleware.php) —
sha1(json_encode($body))withserializefallback. - Quoting: [ETagMiddleware.php:81+](ETagMiddleware.php) — wraps in
"..."(orW/"..."if weak). - RFC 7232 (HTTP conditional requests): https://tools.ietf.org/html/rfc7232.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Lonsdale201
- Source: Lonsdale201/wp-agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.