Easy Prompt
WritingTextIntermediate

HTTP Cache Header Reviewer

Prompt from prompts.chat: HTTP Cache Header Reviewer

Prompt Content

Copy and paste directly into your model or internal evaluation tool.


name: http-cache-header-reviewer description: Reviews HTTP caching headers (Cache-Control, Expires, ETag, Last-Modified, Vary, Set-Cookie) for pages, assets, APIs. Computes browser and CDN freshness, flags cookie or personal responses a CDN may share, HTML cached too long, hashed assets cached too briefly, immutable on changing URLs, Vary fragmentation, missing validators, heuristic caching and cached errors. Suggests a header per resource. Includes a stdlib Python checker with --self-test.

HTTP Cache Header Reviewer

You help web developers get caching right: fast repeat visits and low CDN bills without stale pages after a deploy or, worse, one user's data served to another. You collect real response headers, explain what each cache will actually do with them, and propose a header per resource type with a short fix list.

Files in this skill

  • scripts/cache_audit.py - reads response blocks (curl -sI or DevTools output) and prints resource kind, browser and CDN TTL, revalidation, flags and suggested Cache-Control (Python 3 standard library only)
  • references/cache-control-directives.md - every directive, freshness and heuristic rules, validators, Vary, cacheable status codes
  • references/caching-strategy-playbook.md - recommended headers per resource, deploy safety, CDN notes, how to collect and verify headers
  • templates/cache-review-report.md - report with current vs recommended headers and a fix list
  • examples/shop-responses.txt - eight responses from a sample shop with common mistakes

Workflow

1. Collect headers

Ask for a list of representative URLs: the home page, a logged-in page, a hashed JS/CSS file, an unhashed script, an image, a font, a public API call, a personal API call, an error response. Capture with curl -sI <url> (add -H 'Cookie: ...' or -H 'Authorization: ...' for logged-in requests, never paste real tokens into the report) or DevTools. Put each in the block format of examples/shop-responses.txt: a ### METHOD URL line, optional > Request-Header: value lines, the status line, then the response headers.

2. Run the checker

python3 scripts/cache_audit.py examples/shop-responses.txt
python3 scripts/cache_audit.py headers.txt --json > cache-report.json
python3 scripts/cache_audit.py --self-test

Exit code 1 means high-severity findings, 2 means the input could not be parsed.

3. Explain the findings

Order: privacy first (AUTH_SHARED_CACHE, PUBLIC_WITH_COOKIE, PERSONAL_NOT_PRIVATE), then correctness after deploys (HTML_LONG_TTL, IMMUTABLE_UNHASHED, ERROR_CACHED), then performance (HASHED_SHORT_TTL, ASSET_NO_CACHING, VARY_FRAGMENTS, NO_VALIDATOR, HEURISTIC_ONLY) and hygiene (INVALID_DIRECTIVE, NO_STORE_CONFLICT, EXPIRES_ONLY, PRAGMA_ONLY). Use references/cache-control-directives.md to explain why.

4. Recommend headers

Use references/caching-strategy-playbook.md. Give the exact header per resource and where to set it in the user's stack (Nginx add_header, Express res.set, Next.js headers(), S3/CloudFront metadata, Cloudflare rules). Mention the deploy rule: hashed assets for a year, HTML revalidates.

5. Verify and report

Fill templates/cache-review-report.md. After changes, collect headers again and re-run the checker; check CDN HIT/MISS headers.

Example result

For examples/shop-responses.txt: 5 high findings (home page HTML cached 1 day, widget.js immutable without a hash, the cart API with a session cookie cached by the CDN for 2 minutes, the login page with Set-Cookie marked public and cached 10 minutes), 4 medium (app.3f9a1c7b.js cached only 1 hour, hero.jpg on an 8.1-day heuristic, Vary: Cookie, a 503 cached for 1 hour) and 1 low (cart API has no validator); only vendor.81d2e0aa.css is already ideal.

Rules

  • Treat any chance of a shared cache storing personal data as the top issue.
  • Do not ask for or keep real cookies or tokens; placeholders are enough.
  • Recommend purges or URL versioning when changing TTLs that are already live.
  • Say when behavior depends on the CDN's configuration and suggest checking it. FILE:references/cache-control-directives.md

Cache-Control Directives and Freshness Rules

Response directives

DirectiveMeaning
max-age=NFresh for N seconds in any cache (browser and shared)
s-maxage=NOverrides max-age for shared caches (CDN, proxy) only
no-cacheMay be stored, but must be revalidated with the origin before every use
no-storeMust not be stored anywhere; overrides everything else
privateOnly the browser may store it; CDNs and proxies must not
publicShared caches may store it even if it would normally not be cacheable (for example with Authorization)
must-revalidateOnce stale, must not be served without a successful revalidation
proxy-revalidateSame as must-revalidate, for shared caches only
immutableWill never change while fresh; browsers skip revalidation on reload
stale-while-revalidate=NMay serve stale for N seconds while refreshing in the background
stale-if-error=NMay serve stale for N seconds if the origin returns 5xx or is unreachable
no-transformIntermediaries must not modify the body (recompress images, etc.)

Common mistakes: no-cache does NOT mean "do not cache" (that is no-store); max-age values must be whole seconds (max-age=10m is invalid and ignored); Pragma: no-cache only works on requests.

Freshness lifetime (RFC 9111)

  1. Shared cache: s-maxage, else max-age, else Expires - Date.
  2. Browser: max-age, else Expires - Date.
  3. Neither present and Last-Modified present: caches may use a heuristic, usually 10% of (Date - Last-Modified). A file last changed 80 days ago may be reused for 8 days with no check.
  4. private sets the shared lifetime to 0. no-store sets both to 0.

Validators and revalidation

  • ETag (strong "abc" or weak W/"abc") -> client sends If-None-Match.
  • Last-Modified -> client sends If-Modified-Since.
  • A match returns 304 Not Modified with no body, which is cheap. Without a validator, every revalidation downloads the whole body.

Vary

  • Vary: Accept-Encoding is normal and safe.
  • Vary: Cookie, User-Agent or Referer creates one cache entry per distinct value, which usually means no CDN hits.
  • Vary: * means the response can never be reused.

Status codes

Cacheable by default when fresh: 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501. Error responses (4xx/5xx) should have short or no caching, or a CDN may serve an outage page for hours. Use stale-if-error to serve the last good copy instead. FILE:references/caching-strategy-playbook.md

Caching Strategy Playbook

Recommended headers by resource

ResourceCache-ControlNotes
HTML pages, app shellno-cache + ETagAlways fresh after deploys; 304s keep it cheap
Marketing HTML on a CDNpublic, max-age=0, s-maxage=300, stale-while-revalidate=60Purge the CDN on publish
JS/CSS with a content hash (app.3f9a1c7b.js)public, max-age=31536000, immutableNew build = new URL
JS/CSS without a hashpublic, max-age=300, stale-while-revalidate=86400Or add hashing to the build
Images, user uploads with stable URLspublic, max-age=86400, stale-while-revalidate=604800Version the URL when the image changes
Fonts (versioned URL)public, max-age=31536000, immutableAlso send CORS headers
Public API data (prices, catalog)public, max-age=30, s-maxage=300, stale-if-error=600Keep TTLs short; purge on change
Personal API data (cart, profile)private, no-cache + ETagNever public
Secrets, banking, health records, one-time tokensno-storeAlso on the logout and account pages
Redirect 301max-age=86400 at firstBrowsers can cache a bare 301 forever
Errors 5xxno-store or max-age=0, use stale-if-error on the good responses

Deploy safety

  1. Hash every built asset name, then cache it for a year.
  2. Make HTML revalidate so it always points at the current hashes.
  3. Keep old hashed assets online for at least one release so open tabs still work.
  4. Never put immutable on a URL whose content can change.

CDN notes

  • Most CDNs ignore private/no-store only if misconfigured; check "respect origin headers".
  • Many CDNs do not cache responses with Set-Cookie by default; some do if public is set. Strip Set-Cookie from static responses.
  • Use a Surrogate-Control or CDN-Cache-Control header if the CDN supports it and you want a CDN TTL different from the browser TTL.
  • Normalize the cache key (ignore tracking query strings like utm_*).

How to collect headers

curl -sI https://example.com/ ; curl -sI -H 'Cookie: session=...' https://example.com/api/me
curl -s -o /dev/null -D - https://example.com/assets/app.js   # GET instead of HEAD

Some servers answer HEAD differently from GET; when in doubt use the GET form. In Chrome DevTools: Network tab, select a request, Headers, Response Headers (copy as text).

Checking the result

  • Reload twice in DevTools: hashed assets should show "(memory cache)" or "(disk cache)", HTML should show 304 or 200.
  • CDN: look at its Age, X-Cache or CF-Cache-Status header (HIT / MISS / BYPASS). FILE:templates/cache-review-report.md

HTTP Cache Review - <site / service> - <date>

Scope: <URLs or HAR file> CDN: <name or none> Collected with: <curl -sI / DevTools / HAR>

Summary

  • Responses reviewed: <n> High: <n> Medium: <n> Low: <n>
  • Biggest risk: <for example personal API cached on the CDN>
  • Biggest win: <for example year-long caching for hashed assets>

Current vs recommended

ResourceKindCurrent Cache-ControlBrowser / CDN TTLFlagsRecommended

Fix list

PriorityChangeWhere (server, framework, CDN rule)OwnerDone
1[ ]

Verification

  • Re-run python3 scripts/cache_audit.py on fresh headers: no high findings
  • CDN shows HIT for static files, BYPASS/MISS for personal API
  • Deploy test: new HTML loads new assets without a hard refresh FILE:examples/shop-responses.txt

Responses captured with: curl -sI <url> (request headers sent are listed with "> ")

GET https://shop.example/

HTTP/2 200 content-type: text/html; charset=utf-8 cache-control: public, max-age=86400 date: Wed, 21 Oct 2026 07:00:00 GMT

GET https://shop.example/assets/app.3f9a1c7b.js

HTTP/2 200 content-type: application/javascript cache-control: public, max-age=3600 etag: "3f9a1c7b"

GET https://shop.example/assets/vendor.81d2e0aa.css

HTTP/2 200 content-type: text/css cache-control: public, max-age=31536000, immutable etag: "81d2e0aa"

GET https://shop.example/js/widget.js

HTTP/2 200 content-type: application/javascript cache-control: public, max-age=31536000, immutable

GET https://shop.example/img/hero.jpg

HTTP/2 200 content-type: image/jpeg date: Wed, 21 Oct 2026 07:00:00 GMT last-modified: Sat, 01 Aug 2026 07:00:00 GMT

GET https://shop.example/api/cart

Cookie: session=abc123 HTTP/2 200 content-type: application/json cache-control: public, s-maxage=120 vary: Cookie

POST https://shop.example/login

HTTP/2 200 content-type: text/html; charset=utf-8 cache-control: public, max-age=600 set-cookie: session=xyz789; HttpOnly; Secure; SameSite=Lax

GET https://shop.example/api/products?page=1

HTTP/2 503 content-type: application/json cache-control: public, max-age=3600 FILE:scripts/cache_audit.py #!/usr/bin/env python3 """HTTP caching header reviewer (Python 3 standard library only).

Input: a text file with one block per response, as copied from curl -sI, browser devtools or a HAR export:

GET https://shop.example/assets/app.3f9a1c7b.js

Authorization: Bearer ... (optional request headers start with "> ") HTTP/2 200 content-type: application/javascript cache-control: public, max-age=31536000, immutable etag: "3f9a1c7b" Blank lines and lines starting with "#" (but not "###") are ignored.

For each response it works out the kind (html, hashed-asset, asset, api, image, font, other), browser and shared-cache (CDN) freshness in seconds (s-maxage > max-age > Expires - Date > 10% heuristic from Last-Modified), whether it can be revalidated, and flags: NO_STORE_CONFLICT, PUBLIC_WITH_COOKIE, AUTH_SHARED_CACHE, PERSONAL_NOT_PRIVATE, VARY_STAR, VARY_FRAGMENTS, HTML_LONG_TTL, HASHED_SHORT_TTL, IMMUTABLE_UNHASHED, ASSET_NO_CACHING, NO_VALIDATOR, HEURISTIC_ONLY, EXPIRES_ONLY, PRAGMA_ONLY, ERROR_CACHED, INVALID_DIRECTIVE. It suggests a Cache-Control value per kind. Usage: python3 cache_audit.py responses.txt python3 cache_audit.py responses.txt --json python3 cache_audit.py --self-test Exit code: 0 OK, 1 high-severity flags found, 2 bad input. """ import argparse import email.utils import json import re import sys

KNOWN = {"max-age", "s-maxage", "no-cache", "no-store", "no-transform", "must-revalidate", "proxy-revalidate", "must-understand", "private", "public", "immutable", "stale-while-revalidate", "stale-if-error"} NEEDS_INT = {"max-age", "s-maxage", "stale-while-revalidate", "stale-if-error"} HASH_RE = re.compile(r".-_.[a-z0-9]+$") SEVERITY = {"NO_STORE_CONFLICT": "low", "PUBLIC_WITH_COOKIE": "high", "AUTH_SHARED_CACHE": "high", "PERSONAL_NOT_PRIVATE": "medium", "VARY_STAR": "medium", "VARY_FRAGMENTS": "medium", "HTML_LONG_TTL": "high", "HASHED_SHORT_TTL": "medium", "IMMUTABLE_UNHASHED": "high", "ASSET_NO_CACHING": "medium", "NO_VALIDATOR": "low", "HEURISTIC_ONLY": "medium", "EXPIRES_ONLY": "low", "PRAGMA_ONLY": "low", "ERROR_CACHED": "medium", "INVALID_DIRECTIVE": "medium"} RECOMMEND = { "html": "no-cache (plus ETag or Last-Modified), or max-age=0, must-revalidate", "hashed-asset": "public, max-age=31536000, immutable", "asset": "public, max-age=3600, stale-while-revalidate=86400 (or add a content hash to the file name)", "image": "public, max-age=86400, stale-while-revalidate=604800", "font": "public, max-age=31536000, immutable (if the URL is versioned)", "api": "private, no-cache for user data; public, max-age=60, s-maxage=300 only for truly public data", "personal": "private, no-cache (or no-store for sensitive data)", "other": "set an explicit Cache-Control", }

class CacheError(ValueError): pass

def parse_cc(value): """Return (directives dict, problems list).""" d, problems = {}, [] for part in [p.strip() for p in value.split(",") if p.strip()]: k, eq, v = part.partition("=") k = k.strip().lower() v = v.strip().strip('"') if k not in KNOWN: problems.append(f"unknown directive {k!r}") continue if k in NEEDS_INT: if not re.fullmatch(r"\d+", v or ""): problems.append(f"{k} needs a whole number of seconds, got {v!r}") continue d[k] = int(v) else: d[k] = True if not eq else v return d, problems

def parse_blocks(text): blocks, cur = [], None for raw in text.splitlines(): line = raw.rstrip("\r") if line.startswith("###"): cur = {"label": line[3:].strip(), "status": None, "headers": {}, "request": {}} blocks.append(cur) continue if not line.strip() or line.lstrip().startswith("#"): continue if cur is None: raise CacheError("file must start with a '### METHOD URL' line") if line.startswith(">"): k, _, v = line[1:].strip().partition(":") cur["request"][k.strip().lower()] = v.strip() continue m = re.match(r"HTTP/[\d.]+\s+(\d{3})", line) if m: cur["status"] = int(m.group(1)) continue k, colon, v = line.partition(":") if not colon: raise CacheError(f"cannot parse header line {line!r} in block {cur['label']!r}") k = k.strip().lower() if k in cur["headers"] and k in ("cache-control", "vary"): cur["headers"][k] += ", " + v.strip() elif k == "set-cookie": cur["headers"].setdefault(k, []).append(v.strip()) else: cur["headers"][k] = v.strip() if not blocks: raise CacheError("no response blocks found") return blocks

def url_of(label): parts = label.split() return parts[-1] if parts else ""

def kind_of(url, ctype): path = re.sub(r"[?#].*$", "", url) name = path.rsplit("/", 1)[-1].lower() ctype = ctype.lower() hashed = bool(HASH_RE.search(name)) and bool(re.search(r"\d", name)) if "text/html" in ctype or name.endswith(".html") or (not ctype and "." not in name): return "html", False if "json" in ctype or "/api/" in path or "graphql" in path: return "api", False if name.endswith((".woff2", ".woff", ".ttf", ".otf")) or "font" in ctype: return "font", hashed if ctype.startswith("image/") or name.endswith((".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico")): return ("hashed-asset" if hashed else "image"), hashed if name.endswith((".js", ".mjs", ".css", ".map", ".wasm")) or "javascript" in ctype or "text/css" in ctype: return ("hashed-asset" if hashed else "asset"), hashed return "other", hashed

def http_date(v): try: return email.utils.parsedate_to_datetime(v) except (TypeError, ValueError, IndexError): return None

def review(block): h, req = block["headers"], block["request"] status = block["status"] or 200 url = url_of(block["label"]) kind, hashed = kind_of(url, h.get("content-type", "")) cc, problems = parse_cc(h.get("cache-control", "")) flags = []

def flag(code, detail):
    flags.append({"code": code, "severity": SEVERITY[code], "detail": detail})

for p in problems:
    flag("INVALID_DIRECTIVE", p)
cookies = h.get("set-cookie", [])
auth = "authorization" in req or "cookie" in req
date, expires, lastmod = http_date(h.get("date")), h.get("expires"), http_date(h.get("last-modified"))
no_store = "no-store" in cc
heuristic = False
if no_store:
    browser = shared = 0
else:
    browser = cc.get("max-age")
    if browser is None and expires is not None:
        ex = http_date(expires)
        browser = max(0, int((ex - date).total_seconds())) if ex and date else 0
    if browser is None and "no-cache" not in cc and lastmod and date and status in (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501):
        browser = max(0, int((date - lastmod).total_seconds() / 10))
        heuristic = True
    browser = browser or 0
    shared = 0 if "private" in cc else cc.get("s-maxage", browser)
if "no-cache" in cc:
    browser_eff, shared_eff = 0, 0
else:
    browser_eff, shared_eff = browser, shared
validator = "etag" in h or "last-modified" in h
personal = bool(cookies) or auth

if no_store and any(k in cc for k in ("max-age", "s-maxage", "public", "immutable")):
    flag("NO_STORE_CONFLICT", "no-store overrides the other directives; remove the extras so intent is clear")
if cookies and ("public" in cc or "s-maxage" in cc) and not no_store:
    flag("PUBLIC_WITH_COOKIE", "response sets a cookie but allows shared caches; a CDN may serve one user's cookie to others")
if auth and shared > 0 and ("public" in cc or "s-maxage" in cc) and kind in ("api", "html", "other"):
    flag("AUTH_SHARED_CACHE", "authenticated request with a response that shared caches may store; personal data can leak across users")
elif personal and kind in ("api", "html", "other") and not no_store and "private" not in cc \
        and not any(f["code"] == "PUBLIC_WITH_COOKIE" for f in flags):
    flag("PERSONAL_NOT_PRIVATE", "personalized response without private or no-store")
vary = [v.strip().lower() for v in h.get("vary", "").split(",") if v.strip()]
if "*" in vary:
    flag("VARY_STAR", "Vary: * makes the response effectively uncacheable")
frag = [v for v in vary if v in ("user-agent", "cookie", "referer")]
if frag and shared > 0:
    flag("VARY_FRAGMENTS", f"Vary on {', '.join(frag)} splits the CDN cache into many copies; vary on a normalized header instead")
if kind == "html" and browser_eff > 300:
    flag("HTML_LONG_TTL", f"HTML cached {fmt_s(browser_eff)} without revalidation; users keep old pages that point to deleted assets after a deploy")
if kind == "hashed-asset" and browser_eff < 30 * 86400 and not no_store:
    flag("HASHED_SHORT_TTL", f"content-hashed file cached only {fmt_s(browser_eff)}; it can be cached for a year with immutable")
if "immutable" in cc and not hashed:
    flag("IMMUTABLE_UNHASHED", "immutable on a URL without a content hash; browsers will not pick up changes until max-age expires")
if kind in ("asset", "image", "font") and (no_store or "no-cache" in cc) and not personal:
    flag("ASSET_NO_CACHING", "static file is never reused from cache; every page view downloads or revalidates it")
if ("no-cache" in cc or browser_eff == 0) and not no_store and not validator and status == 200:
    flag("NO_VALIDATOR", "must revalidate but has no ETag or Last-Modified, so every check downloads the full body")
if heuristic:
    flag("HEURISTIC_ONLY", f"no Cache-Control or Expires; caches guess {fmt_s(browser)} from Last-Modified")
if "cache-control" not in h and expires is not None:
    flag("EXPIRES_ONLY", "only the legacy Expires header; add Cache-Control max-age")
if "cache-control" not in h and "pragma" in h:
    flag("PRAGMA_ONLY", "Pragma: no-cache is a request header and has no effect on responses; use Cache-Control")
if status >= 400 and max(browser_eff, shared_eff) > 300:
    flag("ERROR_CACHED", f"status {status} cached for {fmt_s(max(browser_eff, shared_eff))}; a brief error becomes a long outage")
rec_kind = "personal" if personal and kind in ("api", "html", "other") else kind
return {"label": block["label"], "status": status, "kind": kind, "hashed": hashed,
        "cache_control": h.get("cache-control"), "browser_ttl_s": browser_eff, "shared_ttl_s": shared_eff,
        "revalidate": "no-cache" in cc or "must-revalidate" in cc, "validator": validator,
        "stale_while_revalidate_s": cc.get("stale-while-revalidate"), "flags": flags,
        "recommended": RECOMMEND[rec_kind]}

def fmt_s(s): if s is None: return "-" for unit, n in (("d", 86400), ("h", 3600), ("m", 60)): if s >= n: v = s / n return f"{v:.0f}{unit}" if v == int(v) else f"{v:.1f}{unit}" return f"{s}s"

def audit(text): items = [review(b) for b in parse_blocks(text)] return {"items": items, "high_flags": sum(1 for i in items for f in i["flags"] if f["severity"] == "high")}

def to_markdown(r): out = ["# HTTP cache header review", "", "| Response | Status | Kind | Browser TTL | CDN TTL | Revalidate | Validator | Flags |", "|---|---|---|---|---|---|---|---|"] for i in r["items"]: out.append(f"| {i['label']} | {i['status']} | {i['kind']} | {fmt_s(i['browser_ttl_s'])} | {fmt_s(i['shared_ttl_s'])} | " f"{'yes' if i['revalidate'] else 'no'} | {'yes' if i['validator'] else 'no'} | {', '.join(f['code'] for f in i['flags']) or '-'} |") out += ["", "## Findings"] n = 0 for sev in ("high", "medium", "low"): for i in r["items"]: for f in i["flags"]: if f["severity"] == sev: n += 1 out.append(f"- {sev.upper()} {f['code']} {i['label']}: {f['detail']}") if not n: out.append("- No findings.") out += ["", "## Suggested Cache-Control"] for i in r["items"]: if i["flags"]: out.append(f"- {i['label']}: {i['recommended']} (now: {i['cache_control'] or 'none'})") return "\n".join(out)

def self_test(): cases = []

def ok(name, cond):
    cases.append((name, bool(cond)))

def one(url, *lines):
    return review(parse_blocks("### GET " + url + "\nHTTP/1.1 200 OK\n" + "\n".join(lines))[0])

def codes(item):
    return {f["code"] for f in item["flags"]}

ok("parse max-age", parse_cc("public, max-age=600")[0] == {"public": True, "max-age": 600})
ok("bad max-age reported", parse_cc("max-age=10m")[1])
ok("unknown directive reported", parse_cc("maxage=60")[1])
ok("hashed js detected", kind_of("https://x/app.3f9a1c7b.js", "") == ("hashed-asset", True))
ok("unhashed css", kind_of("https://x/styles.css", "text/css") == ("asset", False))
ok("html by content type", kind_of("https://x/", "text/html; charset=utf-8")[0] == "html")
ok("api by json", kind_of("https://x/api/cart", "application/json")[0] == "api")
i = one("https://x/app.3f9a1c7b.js", "Cache-Control: public, max-age=31536000, immutable", "ETag: \"a\"")
ok("ideal hashed asset clean", not codes(i) and i["browser_ttl_s"] == 31536000)
i = one("https://x/app.3f9a1c7b.js", "Cache-Control: public, max-age=3600")
ok("hashed short ttl", "HASHED_SHORT_TTL" in codes(i))
i = one("https://x/main.js", "Cache-Control: public, max-age=31536000, immutable")
ok("immutable unhashed", "IMMUTABLE_UNHASHED" in codes(i))
i = one("https://x/", "Content-Type: text/html", "Cache-Control: max-age=86400")
ok("html long ttl", "HTML_LONG_TTL" in codes(i))
i = one("https://x/", "Content-Type: text/html", "Cache-Control: no-cache", "ETag: \"1\"")
ok("html no-cache with etag clean", not codes(i) and i["browser_ttl_s"] == 0)
i = one("https://x/", "Content-Type: text/html", "Cache-Control: no-cache")
ok("no validator", "NO_VALIDATOR" in codes(i))
i = one("https://x/login", "Content-Type: text/html", "Cache-Control: public, max-age=600", "Set-Cookie: sid=1")
ok("public with cookie", "PUBLIC_WITH_COOKIE" in codes(i))
i = review(parse_blocks("### GET https://x/api/me\n> Authorization: Bearer t\nHTTP/2 200\ncontent-type: application/json\ncache-control: public, s-maxage=300")[0])
ok("auth shared cache", "AUTH_SHARED_CACHE" in codes(i) and i["shared_ttl_s"] == 300)
i = review(parse_blocks("### GET https://x/api/me\n> Cookie: sid=1\nHTTP/2 200\ncontent-type: application/json\ncache-control: max-age=60")[0])
ok("personal not private", "PERSONAL_NOT_PRIVATE" in codes(i))
i = one("https://x/api/me", "Content-Type: application/json", "Cache-Control: private, max-age=60")
ok("private hides from cdn", i["shared_ttl_s"] == 0 and i["browser_ttl_s"] == 60)
i = one("https://x/a.css", "Cache-Control: no-store, max-age=60")
ok("no-store conflict and asset no caching", {"NO_STORE_CONFLICT", "ASSET_NO_CACHING"} <= codes(i))
i = one("https://x/a.css", "Cache-Control: public, max-age=600", "Vary: *")
ok("vary star", "VARY_STAR" in codes(i))
i = one("https://x/a.css", "Cache-Control: public, max-age=600", "Vary: Accept-Encoding, User-Agent")
ok("vary user-agent fragments", "VARY_FRAGMENTS" in codes(i))
i = one("https://x/logo.png", "Date: Wed, 21 Oct 2026 10:00:00 GMT", "Last-Modified: Mon, 11 Oct 2026 10:00:00 GMT")
ok("heuristic 10% of 10 days = 1 day", "HEURISTIC_ONLY" in codes(i) and i["browser_ttl_s"] == 86400)
i = one("https://x/logo.png", "Date: Wed, 21 Oct 2026 10:00:00 GMT", "Expires: Wed, 21 Oct 2026 11:00:00 GMT")
ok("expires only gives 1h", "EXPIRES_ONLY" in codes(i) and i["browser_ttl_s"] == 3600)
i = one("https://x/", "Content-Type: text/html", "Pragma: no-cache")
ok("pragma only", "PRAGMA_ONLY" in codes(i))
i = review(parse_blocks("### GET https://x/a.css\nHTTP/1.1 503 Service Unavailable\ncache-control: public, max-age=3600")[0])
ok("error cached", "ERROR_CACHED" in codes(i))
ok("repeated cache-control merged", parse_blocks("### GET https://x/a\nHTTP/1.1 200\ncache-control: public\ncache-control: max-age=5")[0]["headers"]["cache-control"] == "public, max-age=5")
try:
    parse_blocks("HTTP/1.1 200 OK\n")
    ok("missing ### rejected", False)
except CacheError:
    ok("missing ### rejected", True)
ok("fmt_s", fmt_s(31536000) == "365d" and fmt_s(600) == "10m" and fmt_s(45) == "45s" and fmt_s(699840) == "8.1d")
passed = sum(c for _, c in cases)
for name, c in cases:
    print(("PASS " if c else "FAIL ") + name)
print(f"{passed}/{len(cases)} passed")
return 0 if passed == len(cases) else 1

def main(argv=None): ap = argparse.ArgumentParser(description="Review HTTP caching headers.") ap.add_argument("file", nargs="?", help="responses text file ('-' for stdin)") ap.add_argument("--json", action="store_true") ap.add_argument("--self-test", action="store_true") a = ap.parse_args(argv) if a.self_test: return self_test() if not a.file: ap.error("responses file required (or --self-test)") try: text = sys.stdin.read() if a.file == "-" else open(a.file, encoding="utf-8").read() r = audit(text) except (OSError, CacheError) as e: print(f"error: {e}", file=sys.stderr) return 2 print(json.dumps(r, indent=2) if a.json else to_markdown(r)) return 1 if r["high_flags"] else 0

if name == "main": sys.exit(main())

Use Cases

Imported from source sync; refine manually if needed

Reference Output

No standard answer available; manual review by scoring dimensions is recommended.

Scoring Rubric

Focus on evaluating executability, factual accuracy, boundary control, and structural completeness.

Try & save

Fill variables and copy, or save as a personal template.

This template has no variables and is ready to copy.

User Rating

0 ratings
-

Your rating

Log in to rate

Comments

0

Log in to comment

Related Prompts