Add better-route 1.1 ETag and If-None-Match handling to GET or HEAD routes. Use for ETagMiddleware, strong or weak validators, custom etagResolver, WP_REST_Response preservation, comma-separated validators, wildcard matching, 304 responses, Cache-Control, proxy-stripped ETag troubleshooting, reviewing conditional HTTP caching, or configuring identity/namespace/URL isolation in CachingMiddleware. The middleware skips WP_Error, 204, redirects, and non-2xx responses.
Installs into .claude/skills of the current project.
Are you the author of Br Etag Cache?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/lonsdale201-br-etag-cache)
---
name: br-etag-cache
description: Add better-route 1.1 ETag and If-None-Match handling to GET or HEAD routes. Use for ETagMiddleware, strong or weak validators, custom etagResolver, WP_REST_Response preservation, comma-separated validators, wildcard matching, 304 responses, Cache-Control, proxy-stripped ETag troubleshooting, reviewing conditional HTTP caching, or configuring identity/namespace/URL isolation in CachingMiddleware. The middleware skips WP_Error, 204, redirects, and non-2xx responses.
metadata:
wp-skills-author: "Soczó Kristóf"
wp-skills-contact: "mailto:lonsdale201@hotmail.com"
wp-skills-plugin: "better-route"
wp-skills-plugin-version-tested: "1.1.1"
wp-skills-php-min: "8.1"
wp-skills-last-updated: "2026-09-21"
---
# better-route: ETag conditional reads
Use ETags on read routes to let clients revalidate a representation. They do not prevent duplicate writes; use idempotency for that.
```php
use BetterRoute\Middleware\Cache\ETagMiddleware;
$router->get('/catalog', $handler)
->publicRoute()
->middleware([new ETagMiddleware()]);
```
The default validator is a quoted SHA-1 of the JSON-encoded response body. It applies only to `GET`/`HEAD` results with status 200–299 except 204.
## 1.1 behavior
The middleware preserves Better Route `Response` and `WP_REST_Response` status/data. It adds the ETag through the appropriate response API instead of flattening the WordPress response.
It skips:
- returned `WP_Error`;
- non-2xx responses;
- `204 No Content`;
- non-GET/HEAD methods.
`If-None-Match` accepts:
- a single validator;
- a comma-separated validator list;
- weak or strong forms of the same opaque tag;
- `*`.
On a match, 1.1 returns `304` with no body and preserves cache-relevant source headers: `Cache-Control`, `Content-Location`, `Expires`, and `Vary`, plus the ETag.
The middleware computes and controls the final `ETag` header; do not rely on an existing handler ETag remaining unchanged.
## Cheap custom validators
For large responses, derive a validator from a stable version instead of hashing the full body:
```php
use BetterRoute\Http\RequestContext;
$etag = new ETagMiddleware(
weak: false,
etagResolver: static function (mixed $response, RequestContext $context): string {
return (string) get_option('myapp_catalog_version', 0);
},
);
```
Return the opaque value; the middleware quotes it. A returned already-quoted or `W/` value is normalized. Invalid quote/control bytes are replaced with a safe hash rather than reaching an HTTP header.
Use `weak: true` when byte differences may represent the same semantic representation:
```php
new ETagMiddleware(weak: true); // W/"..."
```
The default JSON hash follows array order. Deeply sort associative data before returning it, or use a stable version resolver, when construction order is nondeterministic.
## Cache-Control and privacy
ETag enables revalidation; it does not define freshness or sharing. Set Cache-Control separately:
```php
return new Response($data, 200, [
'Cache-Control' => 'public, max-age=300',
]);
```
Use `private`/`no-store` as appropriate for user-specific data. Never let a shared CDN cache `/me` or another personalized URL merely because it has an ETag.
## Server response-cache scope (1.1.1)
`CachingMiddleware` is separate from ETag revalidation. Place authentication before its cache lookup. Default keys include the router namespace, route template, concrete request path, separate URL parameters, identity and merged request parameters. Query/body fields cannot shadow a URL ID into another target's cache entry.
Keep `RequestContext::routePath` as the template; the router supplies `attributes['routeNamespace']`. Custom key resolvers own equivalent namespace/URL/identity/tenant isolation. Test same-template routes across namespaces and different URL IDs with identical shadowing query values.
Old cache entries become cold and expire naturally. For idempotency, the same key-scope fix requires coordinated deployment; follow `br-install-and-migrate` rather than treating write replay like disposable read caching. Rate-limit buckets and optimistic-lock scope are unchanged.
## Troubleshooting
Test both the public endpoint and the PHP/upstream origin. A reverse proxy, nginx/RunCloud rule, CDN, compression layer, or caching plugin may remove or rewrite an outbound ETag even when application-level matching still produces a correct 304.
Verify:
```bash
curl -i 'https://example.com/wp-json/myapp/v1/catalog'
curl -i 'https://example.com/wp-json/myapp/v1/catalog' \
-H 'If-None-Match: "copied-tag"'
```
Also test `If-None-Match: W/"copied-tag"`, a comma-separated list, and `*`.
## Review checklist
- Attach only to GET/HEAD routes.
- Use a cheap stable resolver for large bodies.
- Set explicit Cache-Control and correct privacy semantics.
- Confirm WP REST response status/data/headers survive.
- Confirm 4xx/5xx and WP_Error do not gain an ETag.
- Confirm matched 304 has no body and retains cache headers.
- Inspect intermediary header behavior if ETag disappears externally.
## Related skills
- Use `br-idempotency` or `br-atomic-idempotency` for write retries.
- Use `br-cors-public-client` to expose `ETag` to browser JavaScript.
## References
- Verified source paths:
- `src/Middleware/Cache/ETagMiddleware.php`
- `src/Http/Response.php`