Install
$ agentstack add skill-kilimcininkoroglu-cli-tweaks-http-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 Used
- ● Filesystem access Used
- ✓ 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
HTTP Cache — ETag + Cache-Control
Scan the project's HTTP layer, identify static and dynamic endpoints, and implement content-hash-based ETag caching with Cache-Control headers and 304 Not Modified support.
Default behavior is scan (dry-run). Cache bugs are notoriously sinister — users see stale JS, admins see stale HTML, API responses get cached unexpectedly, and detection often takes days. Apply changes only after reviewing the scan output.
Usage
/http-cache # Scan only — show what would change, don't modify (DEFAULT)
/http-cache scan # Same as default
/http-cache apply # Apply the changes from the scan
How It Works
Phase 1: Project Scan
Detect the project's language and HTTP framework:
| Language | Frameworks to detect | |------------|---------------------------------------------------------------| | Go | net/http, chi, gin, echo, fiber, gorilla/mux | | Node.js | express, fastify, koa, hapi, next.js, nuxt | | Python | flask, django, fastapi, starlette | | Rust | actix-web, axum, warp, rocket | | PHP | laravel, symfony, plain (no framework) | | Java | Spring MVC / Spring Boot, JAX-RS (Jersey), plain Servlets | | C# / .NET | ASP.NET Core (MVC, Razor Pages, Minimal API) | | Ruby | Rails, Sinatra |
Identify:
- Static endpoints — serve files from disk, embed, or public directory (HTML, CSS, JS, JSON, XML, TXT, images, fonts)
- Dynamic endpoints — API routes returning computed data (JSON APIs, SSE, WebSocket)
- Template-rendered pages — HTML generated from templates with dynamic data
- Sensitive endpoints — auth, admin panels, payment, banking, user PII, internal dashboards
- Non-deterministic endpoints — responses intentionally different on every request. Scan handler code for randomness signals:
Math.random,shuffle,rand(,random.sample,random.choice,ORDER BY RAND(), MongoDB$sample, lottery/rotation logic. Caching these breaks their semantics: the browser replays the same "random" payload until max-age expires, and users only escape via hard refresh.
Phase 2: Classify Endpoints
Assign each endpoint a caching strategy:
| Type | Cache-Control | ETag | 304 Support | |-------------------------------------------------|----------------------------------------|--------|-------------| | Immutable assets (JS/CSS with hash in filename) | public, max-age=31536000, immutable | no | no | | Static files (HTML, robots.txt, sitemap, etc.) | public, max-age=3600 | yes | yes | | Landing page / index HTML | public, max-age=300 | yes | yes | | Public API responses (non-sensitive, cacheable) | public, max-age=60 (or as suitable) | yes | yes | | Non-deterministic responses (random selection, shuffle, sampling, rotation) | no-store | no | no | | Real-time API responses (live data) | no-store | no | no | | Template-rendered pages with non-sensitive user data | private, no-cache | yes | yes | | Sensitive pages (auth, admin, payment, banking) | no-store | no | no | | SSE / WebSocket streams | no-store | no | no |
Important distinctions:
no-cache≠ "don't cache" — it means "cache but revalidate every use." For truly sensitive data useno-store.privateallows browser to cache but blocks shared caches (CDN, proxy). On shared devices this is still risky for sensitive content — preferno-store.- For
immutableassets, ETag is unnecessary and counterproductive; some browsers will still send conditional requests, wasting round-trips. - "Public and non-sensitive" does NOT imply "cacheable." A random server sample or shuffled list is public and non-sensitive, yet caching it freezes the randomness — classify by determinism first, sensitivity second.
- One route can span multiple classes. When the same endpoint is deterministic for some query parameters and random for others (e.g.
?country=Xreturns a filtered list but the bare route returns a random sample), set Cache-Control per branch inside the handler — a blanket route-level header will be wrong for at least one variant. stale-while-revalidateandmust-revalidateare opposite refinements ofmax-age.stale-while-revalidate=lets a shared cache serve slightly-stale content while it revalidates in the background — add it to apublic, max-ageasset to hide revalidation latency.must-revalidateforbids serving stale oncemax-ageexpires, forcing a fresh revalidation — use it for content that must never be served stale (pricing, inventory, balances).
Phase 3: Implementation
ETag Generation
Compute a content hash at startup time (not per-request) for embedded/static files. SHA-256 is the safe default — the cost is paid once at startup, so hash speed rarely matters; reach for a faster non-cryptographic hash (xxHash, FNV) only if startup time is measurably affected:
| Language | Hash function | Format | |----------|-------------------------------|-------------------------------------| | Go | crypto/sha256 | fmt.Sprintf("%x", hash) | | Node.js | crypto.createHash('sha256') | '"' + hash.digest('hex') + '"' | | Python | hashlib.sha256() | f'"{h.hexdigest()}"' | | Rust | sha2::Sha256 | format!("\"{}\"", hex) | | PHP | hash_file('sha256', $path) | '"' . hash_file('sha256', $p) . '"' |
ETag value MUST be wrapped in double quotes per RFC 7232: "abc123...".
Strong vs Weak ETags:
- Strong ETag (
"abc123") — bytewise identical content guarantee. - Weak ETag (
W/"abc123") — semantically equivalent content (e.g., same data, different whitespace). Use when: - Computing ETag from metadata (mtime + size) instead of content
- Response goes through compression middleware (gzip/brotli)
- Template-rendered pages where minor formatting differences are acceptable
Note on "startup time": This applies to long-running processes (Go, Node.js, Python servers, Rust). PHP-FPM and request-per-process models compute on demand — cache the hash via APCu or filesystem-derived metadata to avoid hashing on every request.
Conditional Request Handling
Before writing the response body, check the If-None-Match request header. The header may contain a comma-separated list of ETags or *:
If-None-Match: "abc"
If-None-Match: "abc", "def", W/"ghi"
If-None-Match: *
Parse it as a list, not an exact-match string:
func matchETag(ifNoneMatch, etag string) bool {
if ifNoneMatch == "" {
return false
}
if strings.TrimSpace(ifNoneMatch) == "*" {
return true
}
for _, tag := range strings.Split(ifNoneMatch, ",") {
tag = strings.TrimSpace(tag)
// Compare ignoring weak prefix per RFC 7232 §2.3.2 weak comparison
tag = strings.TrimPrefix(tag, "W/")
candidate := strings.TrimPrefix(etag, "W/")
if tag == candidate {
return true
}
}
return false
}
Set these headers on ALL cacheable responses (both 200 and 304):
ETag: "content-hash"Cache-Control:Vary:(see Vary section below)Last-Modified:(optional but recommended for static files — improves proxy/CDN compatibility)
Last-Modified is advertised, not honored, by the manual handlers here. The handlers in this skill validate on ETag / If-None-Match only. A client sending only If-Modified-Since (some proxies, older clients) will get a full 200, not a 304, from these handlers. This is acceptable whenever an ETag is present — modern clients send If-None-Match and revalidate correctly. Add an explicit If-Modified-Since check only if you must serve date-only revalidators. http.ServeContent honors both If-None-Match and If-Modified-Since only when given a NON-zero modtime — the Go example above passes the zero value (stable ETag across restarts), so it is ETag-only too; pass a real file mtime there if you need date-based revalidation. The native framework mechanisms below handle both.
Vary Header
Set Vary whenever the response varies along an axis the cache should distinguish:
| Condition | Required Vary value | |-----------------------------------------------|-------------------------------| | Response uses gzip/brotli compression | Accept-Encoding | | Response varies by language (i18n) | Accept-Language | | Response varies by auth state | Cookie or Authorization | | Content negotiation (JSON vs HTML) | Accept |
Compression + ETag interaction: If a compression middleware (gzip/brotli) sits AFTER your handler, ETag is computed on the uncompressed body but the wire body differs by Accept-Encoding. Two safe options:
- Weak ETag +
Vary: Accept-Encoding— declares semantic equivalence across encodings. - Compute ETag after compression — strong ETag remains valid but couples handler with middleware.
Never use a strong ETag with multiple encoded variants and no Vary — this is a spec violation and can cause cache poisoning across users.
Per-Framework Patterns
Go (net/http) — Prefer http.ServeContent for static files:
The standard library already handles If-None-Match, If-Modified-Since, Range requests, and Content-Type detection. Use it instead of manual implementation when possible:
func cachedFileHandler(content fs.FS, filename, cacheControl string) http.HandlerFunc {
data, err := fs.ReadFile(content, filename)
if err != nil {
return func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not found", http.StatusNotFound)
}
}
etag := fmt.Sprintf(`"%x"`, sha256.Sum256(data))
// Use a STABLE modTime, never time.Now(): time.Now() changes on every process
// restart, so Last-Modified would claim the content changed when it did not.
// The zero Time makes ServeContent omit Last-Modified and rely on the ETag alone
// (verified: ServeContent skips Last-Modified when modTime.IsZero()).
// For files read from disk, pass the real file mtime instead.
var modTime time.Time // zero value
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", cacheControl)
w.Header().Set("ETag", etag)
// ServeContent handles If-None-Match, If-Modified-Since, Range, HEAD,
// and sets Content-Type from filename.
http.ServeContent(w, r, filename, modTime, bytes.NewReader(data))
}
}
Go (net/http) — Manual handler when ServeContent is not suitable:
func contentETag(data []byte) string {
return fmt.Sprintf(`"%x"`, sha256.Sum256(data))
}
func cachedHandler(data []byte, contentType, cacheControl string) http.HandlerFunc {
etag := contentETag(data)
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", cacheControl)
w.Header().Set("ETag", etag)
if matchETag(r.Header.Get("If-None-Match"), etag) {
w.WriteHeader(http.StatusNotModified)
return
}
if r.Method == http.MethodHead {
return
}
w.Write(data)
}
}
Express (Node.js) — Built-in ETag:
Express auto-generates weak ETags by default. For most cases this is enough:
app.set('etag', 'strong'); // or 'weak' (default), or false to disable
For custom static handlers with strong content-hashed ETag:
const crypto = require('crypto');
const fs = require('fs');
function etag(data) {
return '"' + crypto.createHash('sha256').update(data).digest('hex') + '"';
}
function matchETag(header, tag) {
if (!header) return false;
if (header.trim() === '*') return true;
const stripWeak = (s) => s.trim().replace(/^W\//, '');
return header.split(',').some(t => stripWeak(t) === stripWeak(tag));
}
function cachedStatic(filePath, contentType, maxAge) {
const data = fs.readFileSync(filePath);
const tag = etag(data);
return (req, res) => {
res.set('Content-Type', contentType);
res.set('Cache-Control', `public, max-age=${maxAge}`);
res.set('ETag', tag);
if (matchETag(req.get('If-None-Match'), tag)) {
return res.status(304).end();
}
res.send(data);
};
}
FastAPI (Python):
import hashlib
from typing import Optional
from fastapi import Request
from fastapi.responses import Response
def content_etag(data: bytes) -> str:
return f'"{hashlib.sha256(data).hexdigest()}"'
def match_etag(header: Optional[str], tag: str) -> bool:
if not header:
return False
if header.strip() == "*":
return True
strip_weak = lambda s: s.strip().removeprefix("W/")
return any(strip_weak(t) == strip_weak(tag) for t in header.split(","))
def cached_file_response(data: bytes, media_type: str, max_age: int):
etag = content_etag(data)
async def handler(request: Request):
headers = {
"Cache-Control": f"public, max-age={max_age}",
"ETag": etag,
}
if match_etag(request.headers.get("if-none-match"), etag):
return Response(status_code=304, headers=headers)
return Response(content=data, media_type=media_type, headers=headers)
return handler
PHP (plain, no framework) — Weak ETag from metadata:
When computing ETag from filemtime + filesize (not full content hash), use a weak validator. mtime+size cannot guarantee bytewise identity (same-second writes with same size are theoretically possible), so a strong ETag would be a spec violation:
// Weak ETag from file metadata — fast, no hashing per request
$mtime = filemtime($filePath);
$size = filesize($filePath);
$etag = 'W/"' . $mtime . '-' . $size . '"';
header('ETag: ' . $etag);
header('Cache-Control: public, max-age=3600');
// Parse If-None-Match as a list
function matchETag($header, $tag) {
if (!$header) return false;
if (trim($header) === '*') return true;
$stripWeak = fn($s) => preg_replace('/^W\//', '', trim($s));
foreach (explode(',', $header) as $candidate) {
if ($stripWeak($candidate) === $stripWeak($tag)) {
return true;
}
}
return false;
}
if (matchETag($_SERVER['HTTP_IF_NONE_MATCH'] ?? '', $etag)) {
http_response_code(304);
exit;
}
For strong content-based ETag in PHP, use hash_file('sha256', $filePath) and cache the result via APCu to avoid hashing on every request. Always check If-None-Match BEFORE loading/processing data to skip expensive work on cache hits.
Frameworks with native conditional-request support:
Prefer a framework's built-in conditional-request handling over a hand-written handler:
- Spring (Java):
ShallowEtagHeaderFilterauto-generates a content ETag and returns304on anIf-None-Matchmatch. Caveat: it still renders the response (saves bandwidth, not server CPU). For server-side savings plusIf-Match/If-Unmodified-Sincesupport, callServletWebRequest.checkNotModified(etag, lastModified)in the handler and short-circuit before building the body. - ASP.NET Core (C#): Output Caching (
AddOutputCache()+ `app.Us
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: KilimcininKorOglu
- Source: KilimcininKorOglu/cli-tweaks
- 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.