| name | http-cache-tools |
| description | HTTP cache debugging tools and techniques. Use when asked to inspect cache headers, debug HTTP responses, use curl for cache analysis, or verify caching behavior. Includes SparkFabrik container context with make drupal-cli and docker compose commands. |
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-urls to get external URL
curl - The Essential Tool
Basic Header Inspection
curl -sI https://example.com/
curl -si https://example.com/
curl -sIL https://example.com/
Cache-Specific Header Filters
curl -sI https://example.com/ | grep -iE 'x-drupal|cache-control|age|vary|etag'
curl -sI https://example.com/ | grep -iE 'x-drupal|cache|age|vary|etag|expires|pragma|last-modified'
curl -sI https://example.com/ | grep -i 'x-drupal'
Authenticated Requests
curl -sI -H "Cookie: SESSabc123=xyz789" https://example.com/
curl -sI -u username:password https://example.com/
curl -sI -H "Authorization: Bearer TOKEN" https://example.com/
Cache Bypass Testing
curl -sI -H "Cache-Control: no-cache" https://example.com/
curl -sI "https://example.com/?nocache=$(date +%s)"
curl -sI -H "Pragma: no-cache" https://example.com/
Timing and Performance
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/
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:
make drupal-cli
curl -sI http://drupal-nginx/ | grep -iE 'x-drupal|cache-control'
Example 2: Compare Anonymous vs Authenticated
Input: "Check caching difference between anonymous and logged-in users"
Output:
echo "=== Anonymous ==="
curl -sI http://drupal-nginx/node/1 | grep -iE 'x-drupal|cache-control|set-cookie'
echo "=== Authenticated ==="
curl -sI -H "Cookie: SESSxyz=abc123" http://drupal-nginx/node/1 | grep -iE 'x-drupal|cache-control'
Example 3: Debug UNCACHEABLE Response
Input: "Find out why the page shows UNCACHEABLE"
Output:
curl -sI http://drupal-nginx/node/1 | grep -iE 'x-drupal'
Example 4: Test Cache Invalidation
Input: "Verify cache clears after content edit"
Output:
curl -sI http://drupal-nginx/node/1 | grep 'X-Drupal-Cache'
drush php-eval "\Drupal::entityTypeManager()->getStorage('node')->load(1)->save();"
curl -sI http://drupal-nginx/node/1 | grep 'X-Drupal-Cache'
curl -sI http://drupal-nginx/node/1 | grep 'X-Drupal-Cache'
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 6: Full Debug Script
Input: "Give me a complete cache debug for a URL"
Output:
#!/bin/bash
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
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 -n 2 'curl -sI http://drupal-nginx/ | grep -iE "x-drupal|cache-control"'
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:
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:
make drupal-cli-root
apk add --no-cache httpie
Usage
http HEAD http://drupal-nginx/
http http://drupal-nginx/ 'Cookie:SESSxyz=abc'
http --print=h http://drupal-nginx/ | grep -i cache
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:
cache or x-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:
curl -c cookies.txt -X POST \
-d "name=admin&pass=password&form_id=user_login_form&op=Log+in" \
http://drupal-nginx/user/login
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: 0 on 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
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 "└─────────────────────────────────────────────────────────────────┘"
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
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:
./cache-compare.sh http://drupal-nginx/node/1
./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 invalidation
X-Drupal-Cache-Contexts - What the page varies on
X-Drupal-Cache-Max-Age - Minimum max-age from all elements
Container Quick Commands
make drupal-cli
docker compose run --rm -it drupal-tools curl -sI http://drupal-nginx/
docker compose exec php curl -sI http://localhost/