whycachedچرا کش شده
GitHubگیت‌هاب

for developers and AI coding agentsبرای برنامه‌نویس‌ها و ایجنت‌های کدنویسی · v0.1.0

Deployed. Still the old version?دیپلوی شد. هنوز نسخهٔ قدیمی؟

The fix is live, but you still see the old page. whycached sends a few harmless GET requests to the URL you are looking at, compares what comes back, and says which layer is serving the stale copy: a CDN, your browser, a service worker, or an asset that never changes name. It prints the evidence and the fix for that layer, and it never purges anything.اصلاح بالاست ولی هنوز صفحهٔ قدیمی را می‌بینید. whycached چند درخواست GET بی‌خطر به نشانی‌ای که نگاه می‌کنید می‌فرستد، پاسخ‌ها را مقایسه می‌کند و می‌گوید کدام لایه نسخهٔ کهنه را می‌دهد: CDN، مرورگر شما، service worker یا فایلی که نامش هیچ‌وقت عوض نمی‌شود. شاهد و راه‌حل همان لایه را چاپ می‌کند و هیچ‌چیز را purge نمی‌کند.

  • Python
  • MIT
  • 0 deps
  • v0.1.0

    The problemمسئله

    Deployed, but you still see the old version? whycached finds which cache layer is serving it.دیپلوی کردید ولی هنوز نسخهٔ قدیمی را می‌بینید؟ whycached پیدا می‌کند کدام لایهٔ کش آن را می‌دهد.

    The problemمسئله

    You deployed, the fix is live, and the page still shows the old version. Is it the CDN, your browser, a service worker, a one-year max-age on app.js, or did the deploy not finish? People purge everything, hard-reload for an hour, or ask an AI agent that cannot see the headers, and the same correct code is "fixed" again.دیپلوی کردید، اصلاح بالاست و صفحه هنوز نسخهٔ قدیمی را نشان می‌دهد. کار CDN است، مرورگر، service worker، یک سال max-age روی app.js، یا دیپلوی تمام نشده؟ آدم‌ها همه‌چیز را purge می‌کنند، یک ساعت hard reload می‌زنند یا از ایجنتی می‌پرسند که هدرها را نمی‌بیند و همان کد درست دوباره «درست» می‌شود.

    What it doesچه می‌کند

    Point it at the URL you are actually seeing. It sends only GET requests, the same URL four ways (normal twice, cache-busted, with Cache-Control: no-cache, conditional), compares the bytes, and reasons per layer: a shared cache (Cloudflare, Fastly, GitHub Pages, Vercel, CloudFront, Netlify, Akamai and others) that is confirmed stale, how long your browser may legally reuse its copy, a service worker that answers before the network, and assets cached for a year under a name that never changes. Every finding comes with evidence and the fix for that layer. It never purges anything.آن را روی نشانی‌ای که واقعاً می‌بینید بزنید. فقط درخواست GET می‌فرستد، همان نشانی را به چهار شکل (عادی دو بار، با پارامتر شکستن کش، با Cache-Control: no-cache و شرطی)، بایت‌ها را مقایسه می‌کند و برای هر لایه استدلال می‌کند: کش مشترک (Cloudflare، Fastly، GitHub Pages، Vercel، CloudFront، Netlify، Akamai و…) که قطعاً کهنه است، مدتی که مرورگر قانوناً می‌تواند نسخهٔ خود را نگه دارد، service workerی که پیش از شبکه جواب می‌دهد و فایل‌هایی که یک سال با نامی ثابت کش شده‌اند. هر یافته با شاهد و راه‌حل همان لایه می‌آید. هیچ‌چیز را purge نمی‌کند.

    • 4۴request variants compared for every URLنوع درخواست مقایسه‌شده برای هر نشانی
    • 31۳۱tests, local servers only, no internetآزمون، فقط سرور محلی و بدون اینترنت
    • 0۰dependencies · GET requests onlyوابستگی · فقط درخواست GET

    Demoنمایش

    Real output from three broken deploymentsخروجی واقعی از سه دیپلویِ خراب

    Pick a scenario. whycached --demo starts a local server that behaves like a CDN serving an old copy, a PWA with a cache-first service worker, and a healthy site; the probes and the verdicts are real. This is the unedited output (docs/demo.txt). Cloudflare is the label the demo server sends, not a real Cloudflare.یک سناریو را انتخاب کنید. ‎whycached --demo‎ یک سرور محلی راه می‌اندازد که مثل CDN با نسخهٔ قدیمی، یک PWA با service worker از نوع cache-first و یک سایت سالم رفتار می‌کند؛ پروب‌ها و حکم‌ها واقعی‌اند. این خروجی ویرایش‌نشده است (docs/demo.txt). «Cloudflare» برچسبی است که سرور نمایشی می‌فرستد، نه Cloudflare واقعی.

    $ whycachedwhycached --demo

    Full unedited output of the demoخروجی کامل و ویرایش‌نشدهٔ نمایش
    $ whycached http://127.0.0.1:41787/shop/ --expect v2
    whycached 0.1.0 · http://127.0.0.1:41787/shop/
    HTTP 200 · 56 bytes · sha e1ee030ec3f1 · layers: Cloudflare
    
    STALE (1)
    
    ✗ shared · a shared cache (Cloudflare) is serving an older copy than the origin
        why:    the normal response (sha e1ee030ec3f1) differs from the cache-busted one (?_whycached=…, sha ede31a85ec4a)
                a request with 'Cache-Control: no-cache' returns different content (sha ede31a85ec4a) than the normal one (sha e1ee030ec3f1)
                'v2' is MISSING in the normal response and present in the fresh one
                Age: 4120 = the copy you got is 68m 40s old
                it stays fresh for 4h 00m (max-age); about 2h 51m left
                Cloudflare: cf-cache-status: HIT
        fix:    Dashboard > Caching > Configuration > Purge Cache > Custom Purge > the URL
                curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data '{"files":["http://127.0.0.1:41787/shop/"]}'
                long term: send 'Cache-Control: no-cache' (or a short s-maxage) on the HTML, and put a content hash in asset file names
    
    LIKELY (1)
    
    ! browser · your browser can legitimately keep showing the old copy
        why:    Cache-Control: public, max-age=14400
                a browser may reuse its stored copy of this URL for up to 2h 51m more without asking the server at all
        fix:    hard reload: Ctrl+Shift+R (Cmd+Shift+R on macOS), or DevTools > Network > 'Disable cache' while it is open
                or test in a private window; or open http://127.0.0.1:41787/shop/?_whycached=1792823155
                fix for users: send 'Cache-Control: no-cache' (revalidate every time, still cheap with ETag) on the HTML
    
    $ whycached http://127.0.0.1:41787/pwa/
    whycached 0.1.0 · http://127.0.0.1:41787/pwa/
    HTTP 200 · 190 bytes · sha 42348a55d07e · no CDN headers seen
    
    LIKELY (3)
    
    ! service-worker · a service worker may be answering from its own cache
        why:    http://127.0.0.1:41787/pwa/sw.js is registered by the page
                it uses the Cache API; cache names: pwa-v1
                cache-first/precache strategy: it answers from its own cache BEFORE the network, so no HTTP header above matters for those requests
                no skipWaiting(): a new worker waits until every tab of the site is closed
                the worker script itself has Cache-Control: public, max-age=86400: it can be re-fetched from HTTP cache for 24h 00m, so updates are delayed
        fix:    serve the service-worker file with 'Cache-Control: no-cache' (never a long max-age)
                DevTools > Application > Service Workers > Unregister (and tick 'Update on reload'); Application > Storage > Clear site data
                change the cache name/version string in the worker so old caches are deleted in the 'activate' event
    
    ! asset · /static/app.css is cached for 365d 0h under a name that never changes
        why:    Cache-Control: public, max-age=31536000
                the URL has no content hash or version, so after a deploy browsers keep the old file until it expires
        fix:    rename on change (app.3f9a1c.js) or add ?v=<hash>; or shorten max-age and use ETag revalidation
    
    ! asset · /static/app.js is cached for 365d 0h under a name that never changes
        why:    Cache-Control: public, max-age=31536000
                the URL has no content hash or version, so after a deploy browsers keep the old file until it expires
        fix:    rename on change (app.3f9a1c.js) or add ?v=<hash>; or shorten max-age and use ETag revalidation
    
    $ whycached http://127.0.0.1:41787/docs/
    whycached 0.1.0 · http://127.0.0.1:41787/docs/
    HTTP 200 · 148 bytes · sha e7eca88c3067 · no CDN headers seen
    
    OK (1)
    
    ✓ info · no caching layer found that would explain an old version
        why:    normal, repeated, cache-busted and no-cache requests all return the same content
                Cache-Control: no-cache
        fix:    if you still see an old version, check you are on the right URL/environment and that the deploy finished
    

    Featuresامکانات

    What it does, precisely.دقیقاً چه می‌کند.

    01

    A CDN serving an older copyCDN که نسخهٔ قدیمی‌تر می‌دهد

    If the normal response differs from the cache-busted or no-cache one, or your --expect string is missing in one and present in the other, it is STALE: the vendor, the Age of the copy, how long it stays fresh and the purge command for that CDN.اگر پاسخ عادی با پاسخ بدون‌کش یا no-cache فرق داشته باشد، یا رشتهٔ --expect شما در یکی نباشد و در دیگری باشد، نتیجه STALE است: نام فروشنده، Age نسخه، مدت تازگی و دستور purge همان CDN.

    02

    What your browser may legally keepآنچه مرورگر قانوناً می‌تواند نگه دارد

    Computes private-cache freshness, including the 10 % heuristic when only Last-Modified is sent, and says for how long a browser may reuse its copy without asking the server, with the hard-reload recipe.تازگی کش خصوصی را حساب می‌کند، از جمله قاعدهٔ ۱۰٪ وقتی فقط Last-Modified فرستاده شده، و می‌گوید مرورگر تا چه مدت بی‌پرسیدن از سرور می‌تواند نسخهٔ خود را بدهد، با روش hard reload.

    03

    A service worker answering firstservice workerی که اول جواب می‌دهد

    Finds serviceWorker.register(...) in the HTML, reads the worker (Cache API, Workbox, cache-first, skipWaiting, cache names) and flags the classic bug: a worker script served with a long max-age.فراخوانی serviceWorker.register(...) را در HTML پیدا می‌کند، worker را می‌خواند (Cache API، Workbox، cache-first، skipWaiting، نام کش‌ها) و باگ کلاسیک را نشان می‌دهد: فایل worker با max-age طولانی.

    04

    Assets that never change nameفایل‌هایی که نامشان هیچ‌وقت عوض نمی‌شود

    Linked same-origin CSS and JS cached for a day or more under a name with no hash or version: the reason users keep an old app.js after a deploy.فایل‌های CSS و JS هم‌مبدأ که یک روز یا بیشتر با نامی بدون هش یا نسخه کش شده‌اند: دلیل اینکه کاربران پس از دیپلوی app.js قدیمی را نگه می‌دارند.

    05

    Not a cache problem, and it says soمشکل کش نیست، و می‌گوید نیست

    If your --expect string is in no response at all, the cause is not caching: the deploy has not finished, or you are looking at the wrong environment. It stops you from purging a CDN that is innocent.اگر رشتهٔ --expect در هیچ پاسخی نباشد، علت کش نیست: دیپلوی تمام نشده یا محیط اشتباه است. جلوی purge کردن CDNی که بی‌گناه است را می‌گیرد.

    06

    Made for humans and agentsبرای آدم‌ها و ایجنت‌ها

    --json for scripts, exit code 1 when something is STALE or LIKELY, and a snippet for AGENTS.md / CLAUDE.md so an agent runs it before it edits code that was already correct.خروجی --json برای اسکریپت، کد خروج ۱ وقتی چیزی STALE یا LIKELY باشد و یک snippet برای AGENTS.md / CLAUDE.md تا ایجنت پیش از ویرایش کدِ از قبل درست آن را اجرا کند.

    • Python
    • HTTP caching
    • CDN
    • service worker
    • --expect

    Install & quick startنصب و شروع سریع

    Up and running in a minute.در یک دقیقه راه بیندازید.

    1. Install (Python 3.9+, no dependencies)نصب (پایتون ۳٫۹ به بالا، بدون وابستگی)

      pipx install git+https://github.com/mrzroot/whycached
      # or run without installing:
      uvx --from git+https://github.com/mrzroot/whycached whycached --demo
    2. Diagnose a URLتشخیص یک نشانی

      whycached https://example.com/page
      whycached https://example.com/ --expect "v2.4.1"   # a string that must be in the NEW version
      whycached URL -H "Authorization: Bearer ..."       # extra request headers
    3. Offline demo, as a gate or for an agentنمایش آفلاین، به‌عنوان گیت یا برای ایجنت

      whycached --demo     # a local server that behaves like 3 broken deployments
      whycached URL --json  # exit 1 when STALE or LIKELY, 2 when it could not fetch

    How it worksچطور کار می‌کند

    Four steps, no magic.چهار گام، بدون جادو.

    Four requests, same URLچهار درخواست، یک نشانی

    Normal twice (hit/miss transition, Age), with a unique ?_whycached= parameter (close to the origin), with Cache-Control: no-cache, and conditional (If-None-Match / If-Modified-Since). Only GET is ever sent.عادی دو بار (گذار hit/miss، Age)، با پارامتر یکتای ‎?_whycached=‎ (نزدیک به origin)، با Cache-Control: no-cache و شرطی (If-None-Match / If-Modified-Since). فقط GET فرستاده می‌شود.

    Compare the bytesمقایسهٔ بایت‌ها

    If the normal response differs from the cache-busted or no-cache one, or your --expect string is only in the fresh one, a shared cache is confirmed stale. If --expect is in neither, the deploy has not finished.اگر پاسخ عادی با پاسخ بدون‌کش یا no-cache فرق کند یا رشتهٔ --expect فقط در پاسخ تازه باشد، کهنگی کش مشترک تأیید می‌شود. اگر --expect در هیچ‌کدام نباشد دیپلوی تمام نشده است.

    Reason per layerاستدلال برای هر لایه

    Vendor signatures (CF-Cache-Status, x-vercel-cache, X-Cache, Cache-Status and more), private-cache freshness for the browser, the service worker script, and linked CSS/JS names and max-age.امضای فروشنده‌ها (‏CF-Cache-Status، ‏x-vercel-cache، ‏X-Cache، ‏Cache-Status و…)، تازگی کش خصوصی برای مرورگر، فایل service worker و نام و max-age فایل‌های CSS/JS لینک‌شده.

    Four levels, one fix eachچهار سطح، هرکدام یک راه‌حل

    STALE (confirmed), LIKELY (a legitimate reason your browser can show the old copy), NOTE, OK. Each has the vendor-specific purge or the header to send; exit code 1 for STALE or LIKELY, 2 when it could not fetch.STALE (تأییدشده)، LIKELY (دلیل موجهی که مرورگر نسخهٔ قدیمی را نشان دهد)، NOTE و OK. هرکدام دستور purge مخصوص فروشنده یا هدر لازم را دارد؛ کد خروج ۱ برای STALE یا LIKELY و ۲ وقتی دریافت ممکن نشد.

    Honest limitsمحدودیت‌های صادقانه

    What it does not do. Please read.چه کارهایی نمی‌کند. لطفاً بخوانید.

    • It cannot see inside your browser. Browser and service-worker findings are reasons it can happen, never proof that your tab is stale; a hard reload or a private window settles it.درون مرورگر شما را نمی‌بیند. یافته‌های مرورگر و service worker دلیل‌های «ممکن است» هستند، نه اثبات کهنگی تب شما؛ hard reload یا پنجرهٔ خصوصی تکلیف را روشن می‌کند.
    • The cache-busted request is not guaranteed to reach the origin: some CDNs ignore the query string in the cache key and a difference can be hidden. The no-cache probe and --expect are independent cross-checks.درخواست با پارامتر شکستن کش تضمینی ندارد که به origin برسد: بعضی CDNها query string را در کلید کش نادیده می‌گیرند و اختلاف می‌تواند پنهان بماند. پروب no-cache و --expect بررسی‌های مستقل‌اند.
    • Per-user, per-region and per-device caches (CDN points of presence, Vary, cookies) may differ from what this machine sees. Login-protected pages need -H.کش‌های مخصوص کاربر، منطقه و دستگاه (نقاط حضور CDN، ‏Vary، کوکی) ممکن است با آنچه این دستگاه می‌بیند فرق کنند. صفحات پشت ورود به -H نیاز دارند.
    • Header signatures reflect public vendor behaviour as of October 2026 and may drift. Tested on Linux only; macOS and Windows should work (standard library) but are untested.امضای هدرها رفتار عمومی فروشنده‌ها تا اکتبر ۲۰۲۶ را نشان می‌دهد و ممکن است تغییر کند. فقط روی لینوکس آزموده شده؛ macOS و ویندوز باید کار کنند (کتابخانهٔ استاندارد) ولی آزموده نشده‌اند.

    How it comparesمقایسه با ابزارهای دیگر

    Closest things, and the difference.نزدیک‌ترین‌ها و تفاوت‌شان.

    No existing tool found as of Oct 2026تا مهر ۱۴۰۵ (اکتبر ۲۰۲۶) ابزار مشابهی پیدا نشد that probes a live URL with four request variants, names the cache layer serving the stale copy, and gives that layer's fix.که یک نشانی زنده را با چهار نوع درخواست بسنجد، لایهٔ کش دارای نسخهٔ کهنه را نام ببرد و راه‌حل همان لایه را بدهد.

    This is the outcome of searches of GitHub, npm, PyPI and the web on 24 Oct 2026, not a guarantee: search indexes are incomplete and this space changes weekly. If you know one, please open an issue and it will be linked here.این نتیجهٔ جست‌وجو در گیت‌هاب، npm، PyPI و وب در ۲ آبان ۱۴۰۵ است، نه تضمین: نمایه‌های جست‌وجو ناقص‌اند و این حوزه هر هفته عوض می‌شود. اگر موردی می‌شناسید لطفاً یک issue باز کنید تا اینجا پیوند شود.

    ToolابزارWhat it doesچه می‌کندHow it differsتفاوت
    hdrlensOffline CLI that explains a saved header file, including cache freshnessCLI آفلاین که فایل هدر ذخیره‌شده را توضیح می‌دهد، از جمله تازگی کشNo network and one response: it cannot compare layers or find which copy is staleبدون شبکه و برای یک پاسخ: نمی‌تواند لایه‌ها را مقایسه کند یا بگوید کدام نسخه کهنه است
    freshtraceOffline lab for browser/CDN cache policy: paste headers, move through timeآزمایشگاه آفلاین سیاست کش مرورگر/CDN: هدر بچسبانید و زمان را جلو ببریدA simulation of headers you paste; it does not fetch or compareشبیه‌سازی هدرهایی که می‌چسبانید؛ نمی‌گیرد و مقایسه نمی‌کند
    cachetapeReplays a scripted request sequence through a cache model, for CIیک دنبالهٔ درخواست اسکریپتی را روی مدل کش بازپخش می‌کند، برای CIA simulator of an imagined cache, not a probe of yoursشبیه‌ساز یک کش فرضی است، نه پروب کش شما
    Browser DevTools, curl -IShow you the headers of one request, by handهدرهای یک درخواست را به‌صورت دستی نشان می‌دهندYou still have to read Age, X-Cache and Cache-Control yourself and compare variants; whycached automates exactly that checklistخواندن Age، X-Cache و Cache-Control و مقایسهٔ حالت‌ها با خودتان است؛ whycached همین چک‌لیست را خودکار می‌کند
    CDN docs and "check X-Cache and Age" guidesPer-vendor manual checklistsچک‌لیست‌های دستی برای هر فروشندهVendor-specific and manual; whycached applies them to your URL and names the layerمخصوص یک فروشنده و دستی‌اند؛ whycached آن‌ها را روی نشانی شما اعمال می‌کند و لایه را نام می‌برد

    FAQپرسش‌های متداول

    Questions people ask.پرسش‌هایی که می‌پرسند.

    Does it purge or change anything?آیا چیزی را purge یا تغییر می‌دهد؟

    No. It sends only GET requests and prints the purge command; you decide whether to run it.نه. فقط درخواست GET می‌فرستد و دستور purge را چاپ می‌کند؛ اجرایش با شماست.

    What is --expect for?‎--expect برای چیست؟

    Give it a string that should be in the NEW version (a version number, a commit hash, a changed sentence). Then a missing string in the normal response but present in the fresh one is proof, not a hint. If it is in neither, the cause is not caching at all.رشته‌ای بدهید که باید در نسخهٔ جدید باشد (شمارهٔ نسخه، هش کامیت، جمله‌ای که عوض شده). آن‌وقت نبودنش در پاسخ عادی و بودنش در پاسخ تازه اثبات است، نه حدس. اگر در هیچ‌کدام نبود علت اصلاً کش نیست.

    Can it tell me my own browser is stale?آیا می‌تواند بگوید مرورگر خودم کهنه است؟

    No, it runs outside the browser. It tells you for how long a browser may legitimately reuse its copy, which is the LIKELY level. A hard reload or a private window settles it.نه، بیرون از مرورگر اجرا می‌شود. می‌گوید مرورگر تا چه مدت قانوناً می‌تواند نسخهٔ خود را بدهد، یعنی سطح LIKELY. hard reload یا پنجرهٔ خصوصی تکلیف را روشن می‌کند.

    How does an AI agent use it?ایجنت هوش مصنوعی چطور از آن استفاده کند؟

    Paste docs/AGENTS.snippet.md into AGENTS.md or CLAUDE.md so the agent runs whycached before it edits code that was already correct. --json is machine-readable; exit code 1 means STALE or LIKELY.فایل docs/AGENTS.snippet.md را در AGENTS.md یا CLAUDE.md بگذارید تا ایجنت پیش از ویرایش کد درست whycached را اجرا کند. ‎--json‎ ماشین‌خوان است و کد خروج ۱ یعنی STALE یا LIKELY.