http-cache-tools
npx skills add https://github.com/sparkfabrik/sf-awesome-copilot --skill http-cache-tools
Agent 安装分布
Skill 文档
HTTP Cache Debugging Tools
Practical tools and commands for inspecting HTTP cache headers and debugging Drupal caching behavior.
When to Use
- Inspecting cache response headers (X-Drupal-Cache, Cache-Control, etc.)
- Verifying cache hit/miss status
- Debugging why pages aren’t caching
- Testing authenticated vs anonymous caching
- Analyzing Vary headers and cache variations
SparkFabrik Project Context
For container access and service URLs in SparkFabrik projects, see the pkg-skills reference.
Quick reminder:
- Inside container:
http://drupal-nginx - From host: Use
fs-cli pkg:get-urlsto get external URL
curl – The Essential Tool
Basic Header Inspection
# Get headers only (-I = HEAD request, -s = silent)
curl -sI https://example.com/
# GET request with headers shown (-i includes headers in output)
curl -si https://example.com/
# Follow redirects (-L)
curl -sIL https://example.com/
Cache-Specific Header Filters
# Filter Drupal cache headers
curl -sI https://example.com/ | grep -iE 'x-drupal|cache-control|age|vary|etag'
# Full cache header analysis
curl -sI https://example.com/ | grep -iE 'x-drupal|cache|age|vary|etag|expires|pragma|last-modified'
# Just Drupal-specific headers
curl -sI https://example.com/ | grep -i 'x-drupal'
Authenticated Requests
# With session cookie (simulate logged-in user)
curl -sI -H "Cookie: SESSabc123=xyz789" https://example.com/
# With basic auth
curl -sI -u username:password https://example.com/
# With bearer token
curl -sI -H "Authorization: Bearer TOKEN" https://example.com/
Cache Bypass Testing
# Force cache miss with Cache-Control header
curl -sI -H "Cache-Control: no-cache" https://example.com/
# Add random query string to bypass cache
curl -sI "https://example.com/?nocache=$(date +%s)"
# Pragma no-cache (legacy)
curl -sI -H "Pragma: no-cache" https://example.com/
Timing and Performance
# Show timing breakdown
curl -sI -w "\n\nTiming:\n DNS: %{time_namelookup}s\n Connect: %{time_connect}s\n TLS: %{time_appconnect}s\n TTFB: %{time_starttransfer}s\n Total: %{time_total}s\n" https://example.com/
# Simple total time
curl -sI -w "\nTotal time: %{time_total}s\n" https://example.com/
Examples
Example 1: Quick Cache Status Check
Input: “Check if the homepage is being cached”
Output:
# From SparkFabrik container
make drupal-cli
# Inside container
curl -sI http://drupal-nginx/ | grep -iE 'x-drupal|cache-control'
# Expected output for cached page:
# X-Drupal-Cache: HIT
# X-Drupal-Dynamic-Cache: HIT
# Cache-Control: max-age=3600, public
Example 2: Compare Anonymous vs Authenticated
Input: “Check caching difference between anonymous and logged-in users”
Output:
# Anonymous request
echo "=== Anonymous ==="
curl -sI http://drupal-nginx/node/1 | grep -iE 'x-drupal|cache-control|set-cookie'
# Authenticated request (with session cookie)
echo "=== Authenticated ==="
curl -sI -H "Cookie: SESSxyz=abc123" http://drupal-nginx/node/1 | grep -iE 'x-drupal|cache-control'
# Expected:
# Anonymous: X-Drupal-Cache: HIT, Cache-Control: public
# Authenticated: X-Drupal-Dynamic-Cache: HIT, Cache-Control: private
Example 3: Debug UNCACHEABLE Response
Input: “Find out why the page shows UNCACHEABLE”
Output:
# Enable debug headers first (in settings.local.php):
# $settings['http.response.debug_cacheability_headers'] = TRUE;
# Then check all cache metadata
curl -sI http://drupal-nginx/node/1 | grep -iE 'x-drupal'
# Look for:
# X-Drupal-Cache-Max-Age: 0 â Something set max-age to 0
# X-Drupal-Cache-Contexts: user â High-cardinality context
# X-Drupal-Dynamic-Cache: UNCACHEABLE â Page cannot be cached
Example 4: Test Cache Invalidation
Input: “Verify cache clears after content edit”
Output:
# Step 1: Check current cache status
curl -sI http://drupal-nginx/node/1 | grep 'X-Drupal-Cache'
# X-Drupal-Cache: HIT
# Step 2: Edit the node (via UI or drush)
drush php-eval "\Drupal::entityTypeManager()->getStorage('node')->load(1)->save();"
# Step 3: Verify cache miss after invalidation
curl -sI http://drupal-nginx/node/1 | grep 'X-Drupal-Cache'
# X-Drupal-Cache: MISS
# Step 4: Confirm next request is cached
curl -sI http://drupal-nginx/node/1 | grep 'X-Drupal-Cache'
# X-Drupal-Cache: HIT
Example 5: Check Vary Headers
Input: “See what the page varies on”
Output:
curl -sI http://drupal-nginx/ | grep -iE 'vary|x-drupal-cache-contexts'
# Example output:
# Vary: Cookie, Accept-Encoding
# X-Drupal-Cache-Contexts: languages:language_interface theme url.path user.permissions
# The Vary header tells proxies/CDNs what to cache separately
# Cache-Contexts show Drupal's internal cache variations
Example 6: Full Debug Script
Input: “Give me a complete cache debug for a URL”
Output:
#!/bin/bash
# cache-debug.sh - Run from inside drupal-tools container
URL="${1:-http://drupal-nginx/}"
echo "=== Cache Debug for: $URL ==="
echo ""
echo "--- Response Headers ---"
curl -sI "$URL" | grep -iE 'http/|x-drupal|cache|vary|age|etag|expires'
echo ""
echo "--- Timing ---"
curl -sI -w "DNS: %{time_namelookup}s | Connect: %{time_connect}s | TTFB: %{time_starttransfer}s | Total: %{time_total}s\n" -o /dev/null "$URL"
echo ""
echo "--- Cache Status Summary ---"
CACHE=$(curl -sI "$URL" | grep -i 'X-Drupal-Cache:' | cut -d' ' -f2 | tr -d '\r')
DYNAMIC=$(curl -sI "$URL" | grep -i 'X-Drupal-Dynamic-Cache:' | cut -d' ' -f2 | tr -d '\r')
echo "Page Cache: ${CACHE:-N/A}"
echo "Dynamic Cache: ${DYNAMIC:-N/A}"
if [ "$DYNAMIC" = "UNCACHEABLE" ]; then
echo "â ï¸ WARNING: Page is UNCACHEABLE - check for max-age:0"
fi
# Usage from SparkFabrik project
docker compose run --rm -it drupal-tools bash -c 'URL="http://drupal-nginx/node/1"; curl -sI "$URL" | grep -iE "x-drupal|cache"'
Example 7: Watch Cache Headers in Real-Time
Input: “Monitor cache status during development”
Output:
# Watch cache status every 2 seconds
watch -n 2 'curl -sI http://drupal-nginx/ | grep -iE "x-drupal|cache-control"'
# Or with timestamp
while true; do
echo "=== $(date) ==="
curl -sI http://drupal-nginx/ | grep -iE 'x-drupal'
sleep 5
done
Example 8: Test Multiple URLs
Input: “Check cache status for multiple pages”
Output:
# List of URLs to check
URLS=(
"http://drupal-nginx/"
"http://drupal-nginx/node/1"
"http://drupal-nginx/admin"
"http://drupal-nginx/user/login"
)
for url in "${URLS[@]}"; do
echo "=== $url ==="
curl -sI "$url" | grep -iE 'x-drupal-cache|x-drupal-dynamic' || echo "No cache headers"
echo ""
done
Alternative to curl: httpie
httpie provides more readable syntax with colorized output.
Installation
If not present in the drupal-tools container:
# Enter container as root
make drupal-cli-root
# Install httpie
apk add --no-cache httpie
Usage
# Headers only
http HEAD http://drupal-nginx/
# With specific headers
http http://drupal-nginx/ 'Cookie:SESSxyz=abc'
# Filter headers
http --print=h http://drupal-nginx/ | grep -i cache
# Compare anonymous vs authenticated
http --print=h HEAD http://drupal-nginx/node/1
http --print=h HEAD http://drupal-nginx/node/1 'Cookie:SESSxyz=abc'
Browser DevTools
For visual debugging:
- Network tab â Select request â Headers section
- Filter by:
cacheorx-drupal - Disable cache: Network tab â Check “Disable cache”
- Preserve log: Keep requests across navigation
DevTools Cache Headers to Check
| Header | Location | Meaning |
|---|---|---|
X-Drupal-Cache |
Response | Page Cache status |
X-Drupal-Dynamic-Cache |
Response | Dynamic Cache status |
Cache-Control |
Response | Browser/proxy caching rules |
Age |
Response | Seconds since cached by proxy |
Vary |
Response | What causes cache variations |
Quick Reference
| Task | Command |
|---|---|
| Check cache status | curl -sI URL | grep -i x-drupal |
| Full headers | curl -sI URL |
| Authenticated | curl -sI -H "Cookie: SESS=x" URL |
| Bypass cache | curl -sI -H "Cache-Control: no-cache" URL |
| Timing | curl -sI -w "TTFB: %{time_starttransfer}s\n" URL |
| Follow redirects | curl -sIL URL |
Anonymous vs Authenticated Cache Analysis
This section helps analyze how Drupal caches pages for anonymous and authenticated users.
Step-by-Step Analysis
1. Get a Valid Session Cookie
First, log in to Drupal and extract the session cookie:
# Option A: From browser DevTools
# 1. Log in to Drupal
# 2. Open DevTools â Application â Cookies
# 3. Copy the SESS* cookie value (e.g., SESSabc123=xyz789)
# Option B: Via curl (if you have credentials)
curl -c cookies.txt -X POST \
-d "name=admin&pass=password&form_id=user_login_form&op=Log+in" \
http://drupal-nginx/user/login
# Extract session cookie
cat cookies.txt | grep SESS
2. Compare Anonymous vs Authenticated Headers
URL="http://drupal-nginx/node/1"
echo "========== ANONYMOUS REQUEST =========="
curl -sI "$URL" | grep -iE 'http/|x-drupal|cache-control|set-cookie|vary'
echo ""
echo "========== AUTHENTICATED REQUEST =========="
curl -sI -H "Cookie: SESSxxxxxxx=yyyyyyyy" "$URL" | grep -iE 'http/|x-drupal|cache-control|vary'
3. Interpret the Results
Key headers to analyze:
| Header | Anonymous (expected) | Authenticated (expected) | Meaning |
|---|---|---|---|
X-Drupal-Cache |
HIT or MISS |
Not present | Page Cache (only for anonymous) |
X-Drupal-Dynamic-Cache |
HIT |
HIT or UNCACHEABLE |
Dynamic Page Cache |
Cache-Control |
max-age=X, public |
max-age=0, private, no-cache |
Browser/proxy caching |
Vary |
Cookie, Accept-Encoding |
Cookie, Accept-Encoding |
Cache variations |
Set-Cookie |
May set session | Should not set new session | Session handling |
Understanding Cache Behavior
Scenario 1: Optimal Caching (Anonymous)
X-Drupal-Cache: HIT
X-Drupal-Dynamic-Cache: HIT
Cache-Control: max-age=3600, public
â Good: Page is fully cached, served from Page Cache.
Scenario 2: Dynamic Cache Only (Anonymous)
X-Drupal-Cache: MISS
X-Drupal-Dynamic-Cache: HIT
Cache-Control: max-age=3600, public
â ï¸ Partial: Page uses Dynamic Cache but not Page Cache. Check if there are session cookies being set.
Scenario 3: Uncacheable (Anonymous)
X-Drupal-Cache: MISS
X-Drupal-Dynamic-Cache: UNCACHEABLE
Cache-Control: must-revalidate, no-cache, private
â Problem: Page cannot be cached. Check for:
max-age: 0on render elements- High-cardinality cache contexts (e.g.,
user) - Session being started unexpectedly
Scenario 4: Authenticated User (Expected)
X-Drupal-Dynamic-Cache: HIT
Cache-Control: max-age=0, private, no-cache
â Expected: Authenticated pages should be private, Dynamic Cache can still help.
Scenario 5: Authenticated User Uncacheable
X-Drupal-Dynamic-Cache: UNCACHEABLE
Cache-Control: must-revalidate, no-cache, private
â ï¸ Check: Even for authenticated users, Dynamic Cache should work. Look for max-age: 0 issues.
Full Comparison Script
#!/bin/bash
# cache-compare.sh - Compare anonymous vs authenticated caching
URL="${1:-http://drupal-nginx/}"
SESSION_COOKIE="${2:-}"
echo "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
echo "â Cache Analysis: $URL"
echo "ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
echo ""
echo "âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
echo "â ANONYMOUS REQUEST â"
echo "âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
ANON_HEADERS=$(curl -sI "$URL")
echo "$ANON_HEADERS" | grep -iE 'http/|x-drupal|cache-control|vary|set-cookie'
ANON_PAGE_CACHE=$(echo "$ANON_HEADERS" | grep -i 'X-Drupal-Cache:' | awk '{print $2}' | tr -d '\r')
ANON_DYN_CACHE=$(echo "$ANON_HEADERS" | grep -i 'X-Drupal-Dynamic-Cache:' | awk '{print $2}' | tr -d '\r')
ANON_CACHE_CTRL=$(echo "$ANON_HEADERS" | grep -i 'Cache-Control:' | cut -d':' -f2 | tr -d '\r')
echo ""
echo "Summary:"
echo " Page Cache: ${ANON_PAGE_CACHE:-N/A}"
echo " Dynamic Cache: ${ANON_DYN_CACHE:-N/A}"
echo " Cache-Control: ${ANON_CACHE_CTRL:-N/A}"
if [ -n "$SESSION_COOKIE" ]; then
echo ""
echo "âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
echo "â AUTHENTICATED REQUEST â"
echo "âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
AUTH_HEADERS=$(curl -sI -H "Cookie: $SESSION_COOKIE" "$URL")
echo "$AUTH_HEADERS" | grep -iE 'http/|x-drupal|cache-control|vary'
AUTH_DYN_CACHE=$(echo "$AUTH_HEADERS" | grep -i 'X-Drupal-Dynamic-Cache:' | awk '{print $2}' | tr -d '\r')
AUTH_CACHE_CTRL=$(echo "$AUTH_HEADERS" | grep -i 'Cache-Control:' | cut -d':' -f2 | tr -d '\r')
echo ""
echo "Summary:"
echo " Dynamic Cache: ${AUTH_DYN_CACHE:-N/A}"
echo " Cache-Control: ${AUTH_CACHE_CTRL:-N/A}"
fi
echo ""
echo "âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
echo "â DIAGNOSIS â"
echo "âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ"
# Anonymous diagnosis
if [ "$ANON_PAGE_CACHE" = "HIT" ]; then
echo "â
Anonymous: Page Cache is working"
elif [ "$ANON_DYN_CACHE" = "HIT" ]; then
echo "â ï¸ Anonymous: Only Dynamic Cache working (Page Cache MISS)"
elif [ "$ANON_DYN_CACHE" = "UNCACHEABLE" ]; then
echo "â Anonymous: Page is UNCACHEABLE - needs investigation"
else
echo "â ï¸ Anonymous: Cache status unclear"
fi
# Authenticated diagnosis
if [ -n "$SESSION_COOKIE" ]; then
if [ "$AUTH_DYN_CACHE" = "HIT" ]; then
echo "â
Authenticated: Dynamic Cache is working"
elif [ "$AUTH_DYN_CACHE" = "UNCACHEABLE" ]; then
echo "â ï¸ Authenticated: Dynamic Cache not working"
fi
if echo "$AUTH_CACHE_CTRL" | grep -q "private"; then
echo "â
Authenticated: Correctly marked as private"
else
echo "â Authenticated: Should be private but isn't!"
fi
fi
Usage:
# Anonymous only
./cache-compare.sh http://drupal-nginx/node/1
# With authenticated comparison
./cache-compare.sh http://drupal-nginx/node/1 "SESSabc123=xyz789"
Common Issues and Solutions
| Symptom | Likely Cause | Solution |
|---|---|---|
Anonymous gets UNCACHEABLE |
Something sets max-age: 0 |
Enable debug headers, check for bad cache metadata |
Anonymous gets Set-Cookie |
Session started for anonymous | Check for code that calls \Drupal::currentUser() early |
Anonymous Cache-Control: private |
Session or user context | Look for user cache context being added |
Page Cache always MISS |
Vary on Cookie + session exists | Ensure anonymous users don’t get sessions |
Authenticated UNCACHEABLE |
max-age: 0 in render array |
Find element setting zero max-age |
Debug Headers
Enable detailed cache debug headers in settings.local.php:
$settings['http.response.debug_cacheability_headers'] = TRUE;
This exposes additional headers:
X-Drupal-Cache-Tags– Cache tags for invalidationX-Drupal-Cache-Contexts– What the page varies onX-Drupal-Cache-Max-Age– Minimum max-age from all elements
Container Quick Commands
# SparkFabrik: Open interactive shell
make drupal-cli
# SparkFabrik: One-off command
docker compose run --rm -it drupal-tools curl -sI http://drupal-nginx/
# Generic Docker Compose
docker compose exec php curl -sI http://localhost/