# Http Cache

> >

- **Type:** Skill
- **Install:** `agentstack add skill-kilimcininkoroglu-cli-tweaks-http-cache`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [KilimcininKorOglu](https://agentstack.voostack.com/s/kilimcininkoroglu)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [KilimcininKorOglu](https://github.com/KilimcininKorOglu)
- **Source:** https://github.com/KilimcininKorOglu/cli-tweaks/tree/main/wrongstack/skills/http-cache

## Install

```sh
agentstack add skill-kilimcininkoroglu-cli-tweaks-http-cache
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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:
1. **Static endpoints** — serve files from disk, embed, or public directory (HTML, CSS, JS, JSON, XML, TXT, images, fonts)
2. **Dynamic endpoints** — API routes returning computed data (JSON APIs, SSE, WebSocket)
3. **Template-rendered pages** — HTML generated from templates with dynamic data
4. **Sensitive endpoints** — auth, admin panels, payment, banking, user PII, internal dashboards
5. **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 use `no-store`.
- `private` allows browser to cache but blocks shared caches (CDN, proxy). On shared devices this is still risky for sensitive content — prefer `no-store`.
- For `immutable` assets, 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=X` returns 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-revalidate` and `must-revalidate` are opposite refinements of `max-age`.** `stale-while-revalidate=` lets a shared cache serve slightly-stale content while it revalidates in the background — add it to a `public, max-age` asset to hide revalidation latency. `must-revalidate` forbids serving stale once `max-age` expires, 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:

```go
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:

1. **Weak ETag + `Vary: Accept-Encoding`** — declares semantic equivalence across encodings.
2. **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:

```go
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:**

```go
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:

```javascript
app.set('etag', 'strong'); // or 'weak' (default), or false to disable
```

For custom static handlers with strong content-hashed ETag:

```javascript
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):**

```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:

```php
// 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):** `ShallowEtagHeaderFilter` auto-generates a content ETag and returns `304` on an `If-None-Match` match. Caveat: it still renders the response (saves bandwidth, not server CPU). For server-side savings plus `If-Match` / `If-Unmodified-Since` support, call `ServletWebRequest.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](https://github.com/KilimcininKorOglu)
- **Source:** [KilimcininKorOglu/cli-tweaks](https://github.com/KilimcininKorOglu/cli-tweaks)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-kilimcininkoroglu-cli-tweaks-http-cache
- Seller: https://agentstack.voostack.com/s/kilimcininkoroglu
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
