<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Cache-Control on Tarragon</title><link>https://tarrragon.github.io/blog/tags/cache-control/</link><description>Recent content in Cache-Control on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 02 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/cache-control/index.xml" rel="self" type="application/rss+xml"/><item><title>12.1 HTTP 回應的保存位置：私有快取與共用快取</title><link>https://tarrragon.github.io/blog/backend/12-http-caching/cache-storage-locations/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/12-http-caching/cache-storage-locations/</guid><description>&lt;p>這篇整理一個 HTTP 回應從 origin（產生回應的源站伺服器）送出、到達使用者之前與之後，可能被另外保存的位置、規範允許保存的條件，以及 &lt;code>Cache-Control&lt;/code> 標頭裡哪些指令只對共用快取或只對私有快取有效。範圍是 RFC 9111 定義的 HTTP 快取；瀏覽器的 back/forward cache 與 Service Worker 自行管理的快取不照這套規則運作，back/forward cache 在 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁&lt;/a> 說明，Service Worker 的快取不在本模組範圍。&lt;/p>
&lt;h2 id="私有快取與共用快取的定義">私有快取與共用快取的定義&lt;/h2>
&lt;p>快取保存下來的回應，本模組稱為&lt;strong>副本&lt;/strong>。RFC 9111 把快取分成兩種，分界是&lt;strong>一份副本會被幾個使用者重用&lt;/strong>：&lt;/p>
&lt;blockquote>
&lt;p>A &amp;ldquo;shared cache&amp;rdquo; is a cache that stores responses for reuse by more than one user; shared caches are usually (but not always) deployed as a part of an intermediary. A &amp;ldquo;private cache&amp;rdquo;, in contrast, is dedicated to a single user; often, they are deployed as a component of a user agent.（RFC 9111 §1）&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>私有快取&lt;/strong>只替一個使用者保存副本，最常見的是瀏覽器的 HTTP 快取：使用者 A 的瀏覽器存下的副本，只會再拿來回應使用者 A 自己的請求。&lt;strong>共用快取&lt;/strong>保存的副本會被拿去回應不同使用者的請求，CDN、放在 origin 前面的反向代理、企業或電信業者的轉送代理都屬於這一種。&lt;/p>
&lt;p>這個分界看的是重用範圍，不看快取放在網路上的哪個位置，原文的「usually」「often」就是在交代這一點。一台放在 origin 同一個機房、由同一個團隊維運的 nginx 反向代理，只要它把同一份副本發給不同使用者，它就是共用快取，所有適用於共用快取的規則都套用在它身上。&lt;/p>
&lt;p>區分兩者的理由在後果：同一份回應如果帶著某個使用者的個人資料，存進私有快取只會回到那個使用者手上，存進共用快取就會被發給別人。規範裡多數只針對共用快取的規則，都在防止這件事。&lt;/p>
&lt;h2 id="各種快取的經營者與-origin-能直接操作的範圍">各種快取的經營者與 origin 能直接操作的範圍&lt;/h2>
&lt;p>一個回應從 origin 到使用者之間，可能依序經過反向代理、CDN、轉送代理，最後到瀏覽器；本模組把路徑上的每一個快取稱為一&lt;strong>層&lt;/strong>。各層的經營者不同，origin 能對它們做的事也不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>快取&lt;/th>
 &lt;th>種類&lt;/th>
 &lt;th>經營者&lt;/th>
 &lt;th>origin 能直接操作的範圍&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>瀏覽器的 HTTP 快取&lt;/td>
 &lt;td>私有&lt;/td>
 &lt;td>使用者自己的裝置&lt;/td>
 &lt;td>無法讀取或刪除；只能透過回應標頭指示，以及只作用於瀏覽器的 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">Clear-Site-Data&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN&lt;/td>
 &lt;td>共用&lt;/td>
 &lt;td>與 origin 簽約的業者&lt;/td>
 &lt;td>透過 CDN 自己提供的管理介面或 API 清除（purge），這是 CDN 的產品功能，不屬於 HTTP 規範&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反向代理（例如 Varnish、nginx 的 proxy_cache）&lt;/td>
 &lt;td>共用&lt;/td>
 &lt;td>origin 自己的團隊&lt;/td>
 &lt;td>設定檔與管理指令都在自己手上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>轉送代理（企業網路、部分電信業者；限明文 HTTP，或會解開 TLS 的企業代理）&lt;/td>
 &lt;td>共用&lt;/td>
 &lt;td>第三方&lt;/td>
 &lt;td>origin 通常不知道它存在，只能透過回應標頭指示&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「origin 能直接操作的範圍」這一欄推出一個前提：&lt;strong>瀏覽器快取與第三方轉送代理沒有任何管理介面可用，origin 想改變它們手上的副本，都要等它們再向 origin 送出請求。&lt;/strong> 轉送代理會在副本過期、回來驗證或重抓時送出請求，經過它的請求帶的快取指令它可以不理（&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁&lt;/a> 的〈請求端快取指令對中介快取的效力〉），origin 不能指望它提早更新；瀏覽器另外還有使用者之後造訪同一個網站時的請求，origin 可以利用那些請求帶上清除指令（&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">Clear-Site-Data&lt;/a>），或由網頁程式以重抓模式覆寫副本、改用新網址繞開副本（&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁&lt;/a> 的〈Fetch 規範的快取模式〉）。例如一個 API 回應誤設 &lt;code>max-age=86400&lt;/code> 上線：企業轉送代理裡的副本，最壞的情況要等這一天過完；使用者瀏覽器裡的副本，可以讓前端改呼叫新的網址，或以 &lt;code>fetch(url, { cache: &amp;quot;reload&amp;quot; })&lt;/code> 重抓一次，兩者都要等使用者再打開這個網站才會發生。簽約的 CDN 同樣不由 origin 經營，但它另外提供清除介面，清得到的範圍限於簽約的那一家。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理一個 HTTP 回應從 origin（產生回應的源站伺服器）送出、到達使用者之前與之後，可能被另外保存的位置、規範允許保存的條件，以及 <code>Cache-Control</code> 標頭裡哪些指令只對共用快取或只對私有快取有效。範圍是 RFC 9111 定義的 HTTP 快取；瀏覽器的 back/forward cache 與 Service Worker 自行管理的快取不照這套規則運作，back/forward cache 在 <a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a> 說明，Service Worker 的快取不在本模組範圍。</p>
<h2 id="私有快取與共用快取的定義">私有快取與共用快取的定義</h2>
<p>快取保存下來的回應，本模組稱為<strong>副本</strong>。RFC 9111 把快取分成兩種，分界是<strong>一份副本會被幾個使用者重用</strong>：</p>
<blockquote>
<p>A &ldquo;shared cache&rdquo; is a cache that stores responses for reuse by more than one user; shared caches are usually (but not always) deployed as a part of an intermediary. A &ldquo;private cache&rdquo;, in contrast, is dedicated to a single user; often, they are deployed as a component of a user agent.（RFC 9111 §1）</p></blockquote>
<p><strong>私有快取</strong>只替一個使用者保存副本，最常見的是瀏覽器的 HTTP 快取：使用者 A 的瀏覽器存下的副本，只會再拿來回應使用者 A 自己的請求。<strong>共用快取</strong>保存的副本會被拿去回應不同使用者的請求，CDN、放在 origin 前面的反向代理、企業或電信業者的轉送代理都屬於這一種。</p>
<p>這個分界看的是重用範圍，不看快取放在網路上的哪個位置，原文的「usually」「often」就是在交代這一點。一台放在 origin 同一個機房、由同一個團隊維運的 nginx 反向代理，只要它把同一份副本發給不同使用者，它就是共用快取，所有適用於共用快取的規則都套用在它身上。</p>
<p>區分兩者的理由在後果：同一份回應如果帶著某個使用者的個人資料，存進私有快取只會回到那個使用者手上，存進共用快取就會被發給別人。規範裡多數只針對共用快取的規則，都在防止這件事。</p>
<h2 id="各種快取的經營者與-origin-能直接操作的範圍">各種快取的經營者與 origin 能直接操作的範圍</h2>
<p>一個回應從 origin 到使用者之間，可能依序經過反向代理、CDN、轉送代理，最後到瀏覽器；本模組把路徑上的每一個快取稱為一<strong>層</strong>。各層的經營者不同，origin 能對它們做的事也不同：</p>
<table>
  <thead>
      <tr>
          <th>快取</th>
          <th>種類</th>
          <th>經營者</th>
          <th>origin 能直接操作的範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>瀏覽器的 HTTP 快取</td>
          <td>私有</td>
          <td>使用者自己的裝置</td>
          <td>無法讀取或刪除；只能透過回應標頭指示，以及只作用於瀏覽器的 <a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">Clear-Site-Data</a></td>
      </tr>
      <tr>
          <td>CDN</td>
          <td>共用</td>
          <td>與 origin 簽約的業者</td>
          <td>透過 CDN 自己提供的管理介面或 API 清除（purge），這是 CDN 的產品功能，不屬於 HTTP 規範</td>
      </tr>
      <tr>
          <td>反向代理（例如 Varnish、nginx 的 proxy_cache）</td>
          <td>共用</td>
          <td>origin 自己的團隊</td>
          <td>設定檔與管理指令都在自己手上</td>
      </tr>
      <tr>
          <td>轉送代理（企業網路、部分電信業者；限明文 HTTP，或會解開 TLS 的企業代理）</td>
          <td>共用</td>
          <td>第三方</td>
          <td>origin 通常不知道它存在，只能透過回應標頭指示</td>
      </tr>
  </tbody>
</table>
<p>「origin 能直接操作的範圍」這一欄推出一個前提：<strong>瀏覽器快取與第三方轉送代理沒有任何管理介面可用，origin 想改變它們手上的副本，都要等它們再向 origin 送出請求。</strong> 轉送代理會在副本過期、回來驗證或重抓時送出請求，經過它的請求帶的快取指令它可以不理（<a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a> 的〈請求端快取指令對中介快取的效力〉），origin 不能指望它提早更新；瀏覽器另外還有使用者之後造訪同一個網站時的請求，origin 可以利用那些請求帶上清除指令（<a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">Clear-Site-Data</a>），或由網頁程式以重抓模式覆寫副本、改用新網址繞開副本（<a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a> 的〈Fetch 規範的快取模式〉）。例如一個 API 回應誤設 <code>max-age=86400</code> 上線：企業轉送代理裡的副本，最壞的情況要等這一天過完；使用者瀏覽器裡的副本，可以讓前端改呼叫新的網址，或以 <code>fetch(url, { cache: &quot;reload&quot; })</code> 重抓一次，兩者都要等使用者再打開這個網站才會發生。簽約的 CDN 同樣不由 origin 經營，但它另外提供清除介面，清得到的範圍限於簽約的那一家。</p>
<h2 id="可以被保存的回應">可以被保存的回應</h2>
<p>規範用一份條件清單規定快取什麼時候<strong>不可以</strong>保存回應（RFC 9111 §3）。規範用大寫的 MUST、SHOULD、MAY 標示要求的強度（RFC 9111 §1.1 引用 RFC 2119 與 RFC 8174 的定義）：MUST 是必須，SHOULD 是除非有充分理由否則應該，MAY 是允許；小寫的同一個字是一般用語。主要條件整理如下，全部滿足才可以保存：</p>
<ul>
<li>請求方法是快取理解、而且允許快取的方法（多數實作只支援 GET 與 HEAD，見 <a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a>）。</li>
<li>回應的狀態碼是最終回應（不是 1xx 這類中間回應）。</li>
<li>狀態碼是 206 或 304、或回應帶 <code>must-understand</code> 指令時，快取要理解這個狀態碼。</li>
<li>回應沒有 <code>no-store</code>。</li>
<li>共用快取另外兩條：回應沒有 <code>private</code>；請求沒有 <code>Authorization</code>，或回應用指令明確允許共用（見 <a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a> 的〈帶 Authorization 的請求在共用快取的重用規則〉）。</li>
<li>回應至少帶有下列一項：<code>public</code>、<code>private</code>（僅限私有快取）、<code>Expires</code>、<code>max-age</code>、<code>s-maxage</code>（僅限共用快取）、允許快取的擴充指令，或狀態碼屬於「可啟發式快取」的種類。</li>
</ul>
<p>清單的最後一條決定了沒有任何快取指令的回應能不能被保存：狀態碼在可啟發式快取的清單裡（例如 200、301），就可以保存，保存之後能直接使用多久由快取自己估算；不在清單裡（例如 302、307），規範不允許保存。這份清單與估算方式在 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a> 的〈沒有明確期限時的啟發式期限〉。</p>
<p>這份清單是規範的要求，而實作的預設可能和它有三種落差，本模組各篇用同一組名稱標出：</p>
<ul>
<li><strong>比規範保守</strong>：規範允許保存或重用的回應，實作不保存或不重用。對 origin 的代價是失去快取的效益，這些請求的負載全部回到 origin。例子是 Varnish 預設不保存 <code>no-cache</code> 的回應（<a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a> 的〈共用快取實作的驗證預設〉）。</li>
<li><strong>比規範寬鬆</strong>：規範預設不允許、但留了出口讓經營者用設定開啟的行為，實作預設就開著。RFC 9111 §4.2.4 預設不給出過期副本，同時允許依「out-of-band contract」的設定給出，Varnish 預設的寬限時間就落在這個出口上（<a href="/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</a>）。代價落在使用者這一端：拿到的舊內容比標頭宣告的期限更舊。</li>
<li><strong>違反規範</strong>：規範沒有留出口的禁止，實作照樣做。代價最直接，可能把一個使用者的回應發給別人。例子是預設設定的 nginx 共用帶 <code>Authorization</code> 請求的回應（<a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a> 的〈帶 Authorization 的請求在共用快取的重用規則〉）；回應明寫 <code>must-revalidate</code> 而 Varnish 仍在寬限時間內給出它的過期副本，也屬於這一類，因為明確的指令禁止沒有設定上的出口（<a href="/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</a>）。</li>
</ul>
<p>三種落差都只能從實作的文件、原始碼或實測得知。規範隱含的保護（例如帶 <code>Authorization</code> 的回應不被共用）不能依賴，要由回應明寫的標頭（<code>private</code>、<code>no-store</code>）承擔；延用過期副本這一類，連明寫的 <code>must-revalidate</code> 都可能被實作的寬限時間略過，要回到快取自己的設定處理。</p>
<h2 id="只對共用快取或只對私有快取有效的指令">只對共用快取或只對私有快取有效的指令</h2>
<p><code>Cache-Control</code> 標頭裡有幾個指令只對共用快取或只對私有快取有效，表中 <code>no-store</code> 列作對照，它對兩種快取都有效：</p>
<table>
  <thead>
      <tr>
          <th>指令</th>
          <th>對私有快取</th>
          <th>對共用快取</th>
          <th>出處</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>private</code></td>
          <td>可以保存（即使回應原本不屬於可啟發式快取的種類）</td>
          <td>不可保存</td>
          <td>RFC 9111 §5.2.2.7</td>
      </tr>
      <tr>
          <td><code>public</code></td>
          <td>標示可保存</td>
          <td>允許把帶 <code>Authorization</code> 標頭的請求所拿到的回應保存下來重用</td>
          <td>RFC 9111 §5.2.2.9</td>
      </tr>
      <tr>
          <td><code>s-maxage</code></td>
          <td>忽略</td>
          <td>取代 <code>max-age</code> 與 <code>Expires</code> 作為新鮮度期限，並帶有 <code>proxy-revalidate</code> 的語意</td>
          <td>RFC 9111 §5.2.2.10</td>
      </tr>
      <tr>
          <td><code>proxy-revalidate</code></td>
          <td>忽略</td>
          <td>過期後必須向 origin 驗證成功才能使用</td>
          <td>RFC 9111 §5.2.2.8</td>
      </tr>
      <tr>
          <td><code>no-store</code></td>
          <td>不可保存</td>
          <td>不可保存</td>
          <td>RFC 9111 §5.2.2.5</td>
      </tr>
  </tbody>
</table>
<p><code>private</code> 只規定保存位置：共用快取不可以存，瀏覽器可以存。規範在同一節明寫它不是保密機制：</p>
<blockquote>
<p>This usage of the word &ldquo;private&rdquo; only controls where the response can be stored; it cannot ensure the privacy of the message content.（RFC 9111 §5.2.2.7）</p></blockquote>
<p><code>public</code> 在多數情形下是多餘的：回應本來就可以被快取時，加上它不改變任何行為（RFC 9111 §5.2.2.9）。它有作用的情形是請求帶了 <code>Authorization</code> 標頭，這種情形在 <a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a> 說明。</p>
<p>本模組的實測環境是 docker 上的一支 Python origin，前面分別接 Varnish 7.7.3 與 nginx 1.27.5，瀏覽器是 Chrome 154。Varnish 只另外加了測試 purge 與 ban 用的規則，一般路徑的請求都走內建設定；nginx 在一般路徑上只加了開啟快取所需的 <code>proxy_cache_path</code> 與 <code>proxy_cache</code>，另有轉送與記錄用的設定，不影響快取判斷。各篇說的「預設設定」都指這個狀態，個別調過參數的測試會在該處寫明。用實測對照規範：回應帶 <code>Cache-Control: private, max-age=60</code> 時，Varnish 7.7 與 nginx 1.27 的 proxy_cache 兩者都沒有保存，每個請求都送到 origin，與規範一致。回應帶 <code>max-age=5, s-maxage=30</code> 時，兩者在第一次請求之後的第 18 秒（<code>max-age</code> 的 5 秒已過、<code>s-maxage</code> 的 30 秒未到）仍由快取直接回應，也就是採用了 <code>s-maxage</code>。</p>
<h2 id="把指令指定給特定快取cdn-cache-control">把指令指定給特定快取：CDN-Cache-Control</h2>
<p><code>s-maxage</code> 能區分「共用快取」與「私有快取」，但無法再細分到某一個共用快取。規範明寫這個限制：</p>
<blockquote>
<p>It is not possible to target a directive to a specific cache.（RFC 9111 §5.2）</p></blockquote>
<p>這個限制的代價是：origin 經常想讓自己簽約的 CDN 把副本當作新鮮久一點（因為 CDN 有清除介面，內容要改時可以主動清掉），同時讓瀏覽器與第三方轉送代理短一點（因為它們清不掉）。用 <code>s-maxage</code> 只能把 CDN 與轉送代理綁在一起設定。</p>
<p>RFC 9213 為這個需求定義了「指定目標的快取控制欄位」：欄位的語意和 <code>Cache-Control</code> 相同，但欄位名稱指明了適用對象。其中 <code>CDN-Cache-Control</code> 適用於代表 origin 運作的分散式快取網路，也就是 CDN。實作這個欄位的快取，在這個欄位有有效值時忽略回應裡的 <code>Cache-Control</code> 與 <code>Expires</code>，改用它（RFC 9213 §2.2）。規範裡的例子：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Cache-Control: max-age=60, s-maxage=120
</span></span><span class="line"><span class="ln">2</span><span class="cl">CDN-Cache-Control: max-age=600</span></span></code></pre></div><p>這組標頭讓 CDN 把副本視為新鮮 600 秒，其他共用快取 120 秒，瀏覽器 60 秒。不支援 <code>CDN-Cache-Control</code> 的快取把它當成一個不認得的標頭欄位：RFC 9110 §5.1 要求代理照樣轉送不認得的欄位，其他接收者應該（SHOULD）忽略這個欄位；實作了 RFC 9213、但 <code>CDN-Cache-Control</code> 不在自己目標清單裡的快取，依 RFC 9213 §2.2 不可以因為這個欄位改變行為。所以加上這個欄位，不支援的快取照 <code>Cache-Control</code> 運作。反過來，實作這個欄位的 CDN 會整個忽略 <code>Cache-Control</code>，包括其中的 <code>private</code>（RFC 9213 §2.2；§5 也提醒這種組合可能讓帶敏感資訊的回應被重用）；Varnish 的內建設定在回應帶 <code>Surrogate-Control</code> 時，不再檢查 <code>Cache-Control</code> 的 <code>no-cache</code>、<code>no-store</code> 與 <code>private</code>，只看 <code>Surrogate-Control</code> 裡有沒有 <code>no-store</code>。個人化的回應要在 <code>CDN-Cache-Control</code> 裡寫 <code>private</code> 或 <code>no-store</code>、在 <code>Surrogate-Control</code> 裡寫 <code>no-store</code>，或不送這些欄位。</p>
<p>在 RFC 9213 之前，部分 CDN 用 <code>Surrogate-Control</code> 這類自訂欄位把指令指定給 CDN。它出自 2001 年的一份 W3C Note（Edge Architecture Specification 1.0），不是標準，RFC 9213 對這類先行做法的評語是「their interoperability is low」。手上的 CDN 支援 <code>CDN-Cache-Control</code>、<code>Surrogate-Control</code> 還是自己的欄位，以該 CDN 的文件為準。</p>
<h2 id="沒有快取標頭的回應的預設處理">沒有快取標頭的回應的預設處理</h2>
<p>規範的預設立場是重用保存的回應：</p>
<blockquote>
<p>it can be assumed that reusing a cached response is desirable and that such reuse is the default behavior when no requirement or local configuration prevents it.（RFC 9111 §2）</p></blockquote>
<p>所以沒有寫 <code>Cache-Control</code> 的回應仍可能被快取：依〈可以被保存的回應〉的最後一條，狀態碼在可啟發式快取的清單裡就能保存，直接使用多久由各快取自行估算，估算方式在 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>。要確定某個回應不被任何快取保存，要明寫 <code>no-store</code>；要讓它可以保存但每次使用前都先問 origin，寫 <code>no-cache</code>，兩者的差別在 <a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a>。</p>
]]></content:encoded></item><item><title>12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</title><link>https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/</guid><description>&lt;p>這篇整理快取判斷一份副本還能不能直接使用的計算方式。範圍從副本已經被保存開始（哪些回應能被保存見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取&lt;/a>）；副本過期之後怎麼處理在 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store&lt;/a>。&lt;/p>
&lt;p>規範用兩個量描述一份副本：&lt;strong>新鮮度期限&lt;/strong>（freshness lifetime，這份副本從 origin 產生起可以被直接使用多久）與&lt;strong>目前年齡&lt;/strong>（current age，這份副本從 origin 產生或最近一次驗證起過了多久）。CDN 的設定介面與 Varnish 的參數把新鮮度期限稱為 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a>（time to live）。目前年齡小於新鮮度期限的那段時間稱為&lt;strong>新鮮期間&lt;/strong>，兩者的差就是副本剩下的新鮮時間。判斷式只有一行：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">response_is_fresh = (freshness_lifetime &amp;gt; current_age)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（RFC 9111 §4.2）新鮮的副本可以不詢問 origin 直接拿來回應請求；過期的副本要先驗證，或在 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error&lt;/a> 說明的條件下延用。&lt;/p>
&lt;h2 id="新鮮度期限的取用順序">新鮮度期限的取用順序&lt;/h2>
&lt;p>快取從回應標頭裡依序找第一個存在的值當新鮮度期限（RFC 9111 §4.2.1）：&lt;/p>
&lt;ol>
&lt;li>快取是共用快取，而且回應有 &lt;code>s-maxage&lt;/code>：用 &lt;code>s-maxage&lt;/code> 的值。&lt;/li>
&lt;li>回應有 &lt;code>max-age&lt;/code>：用 &lt;code>max-age&lt;/code> 的值。&lt;/li>
&lt;li>回應有 &lt;code>Expires&lt;/code>：用 &lt;code>Expires&lt;/code> 減去 &lt;code>Date&lt;/code> 的值；回應沒有 &lt;code>Date&lt;/code> 時，以收到回應的時間代替。&lt;/li>
&lt;li>以上都沒有：沒有明確期限，快取可以改用啟發式期限（見〈沒有明確期限時的啟發式期限〉）。&lt;/li>
&lt;/ol>
&lt;p>用 &lt;code>Expires&lt;/code> 減 &lt;code>Date&lt;/code> 而不是減快取自己的時鐘，是為了避開 origin 與快取之間的時鐘偏差：兩個時間都由 origin 的時鐘產生，相減的結果不受快取那台機器的時間影響。&lt;/p>
&lt;p>幾條處理衝突與異常值的規則：&lt;/p>
&lt;ul>
&lt;li>同時有 &lt;code>max-age&lt;/code> 與 &lt;code>Expires&lt;/code> 時，忽略 &lt;code>Expires&lt;/code>（RFC 9111 §5.3）。&lt;/li>
&lt;li>&lt;code>Expires&lt;/code> 的值無法解析，特別是常見的 &lt;code>Expires: 0&lt;/code>，當成過去的時間處理，也就是回應一送到就已經過期（RFC 9111 §5.3）。&lt;/li>
&lt;li>指令互相衝突（例如同時有 &lt;code>max-age&lt;/code> 與 &lt;code>no-cache&lt;/code>）時，規範用小寫的 should（一般用語，不是 RFC 以大寫 SHOULD 標示的要求）建議採用限制最嚴的那個（RFC 9111 §4.2.1）。&lt;/li>
&lt;/ul>
&lt;h2 id="age-標頭與副本的目前年齡">Age 標頭與副本的目前年齡&lt;/h2>
&lt;p>&lt;code>Age&lt;/code> 標頭記錄的是：&lt;/p>
&lt;blockquote>
&lt;p>the cache&amp;rsquo;s estimate of the number of seconds since the origin server generated or validated the response.（RFC 9111 §4.2.3）&lt;/p>&lt;/blockquote>
&lt;p>快取不經驗證、直接用保存的副本回應請求時，必須在回應裡帶上 &lt;code>Age&lt;/code>，值是這份副本目前的年齡（RFC 9111 §4）。下游的快取收到帶 &lt;code>Age&lt;/code> 的回應，會把這個值算進自己那份副本的年齡。規範給的保守算法還計入網路傳輸的延遲，結構是：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">corrected_initial_age = max(收到時 Date 與自己時鐘的差, 上游給的 Age + 這次請求的往返時間)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">current_age = corrected_initial_age + 副本在自己這裡放了多久&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個機制讓年齡沿著快取鏈累加，影響的是下游拿到的剩餘新鮮時間。一個回應帶 &lt;code>Cache-Control: max-age=600&lt;/code>，在 CDN 上放了 500 秒之後被某個瀏覽器請求，CDN 回給瀏覽器的回應帶 &lt;code>Age: 500&lt;/code>，瀏覽器算出的剩餘新鮮時間就只有 100 秒，不是 600 秒。origin 寫的 &lt;code>max-age&lt;/code> 是從 origin 產生回應起算的總期限，不是每一個快取各自重新計時。&lt;/p>
&lt;p>送不送 &lt;code>Age&lt;/code>，實作之間就有差異。實測時 Varnish 7.7 每次由快取回應都送出遞增的 &lt;code>Age&lt;/code>；nginx 1.27 的 proxy_cache 不送 &lt;code>Age&lt;/code>，而且把 &lt;code>Date&lt;/code> 換成送出當下的時間。下游原本可以用 &lt;code>Date&lt;/code> 與自己時鐘的差估出副本的年齡；&lt;code>Age&lt;/code> 沒送、&lt;code>Date&lt;/code> 又被換成送出當下的時間，下游（例如接在 nginx 外面的 CDN）就估不出來，因此會把副本多用一段時間，長度等於副本在 nginx 裡已經放了的時間。規範也提醒，回應裡沒有 &lt;code>Age&lt;/code> 不代表它剛從 origin 拿來（RFC 9111 §5.1）。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理快取判斷一份副本還能不能直接使用的計算方式。範圍從副本已經被保存開始（哪些回應能被保存見 <a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a>）；副本過期之後怎麼處理在 <a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a>。</p>
<p>規範用兩個量描述一份副本：<strong>新鮮度期限</strong>（freshness lifetime，這份副本從 origin 產生起可以被直接使用多久）與<strong>目前年齡</strong>（current age，這份副本從 origin 產生或最近一次驗證起過了多久）。CDN 的設定介面與 Varnish 的參數把新鮮度期限稱為 <a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a>（time to live）。目前年齡小於新鮮度期限的那段時間稱為<strong>新鮮期間</strong>，兩者的差就是副本剩下的新鮮時間。判斷式只有一行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">response_is_fresh = (freshness_lifetime &gt; current_age)</span></span></code></pre></div><p>（RFC 9111 §4.2）新鮮的副本可以不詢問 origin 直接拿來回應請求；過期的副本要先驗證，或在 <a href="/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</a> 說明的條件下延用。</p>
<h2 id="新鮮度期限的取用順序">新鮮度期限的取用順序</h2>
<p>快取從回應標頭裡依序找第一個存在的值當新鮮度期限（RFC 9111 §4.2.1）：</p>
<ol>
<li>快取是共用快取，而且回應有 <code>s-maxage</code>：用 <code>s-maxage</code> 的值。</li>
<li>回應有 <code>max-age</code>：用 <code>max-age</code> 的值。</li>
<li>回應有 <code>Expires</code>：用 <code>Expires</code> 減去 <code>Date</code> 的值；回應沒有 <code>Date</code> 時，以收到回應的時間代替。</li>
<li>以上都沒有：沒有明確期限，快取可以改用啟發式期限（見〈沒有明確期限時的啟發式期限〉）。</li>
</ol>
<p>用 <code>Expires</code> 減 <code>Date</code> 而不是減快取自己的時鐘，是為了避開 origin 與快取之間的時鐘偏差：兩個時間都由 origin 的時鐘產生，相減的結果不受快取那台機器的時間影響。</p>
<p>幾條處理衝突與異常值的規則：</p>
<ul>
<li>同時有 <code>max-age</code> 與 <code>Expires</code> 時，忽略 <code>Expires</code>（RFC 9111 §5.3）。</li>
<li><code>Expires</code> 的值無法解析，特別是常見的 <code>Expires: 0</code>，當成過去的時間處理，也就是回應一送到就已經過期（RFC 9111 §5.3）。</li>
<li>指令互相衝突（例如同時有 <code>max-age</code> 與 <code>no-cache</code>）時，規範用小寫的 should（一般用語，不是 RFC 以大寫 SHOULD 標示的要求）建議採用限制最嚴的那個（RFC 9111 §4.2.1）。</li>
</ul>
<h2 id="age-標頭與副本的目前年齡">Age 標頭與副本的目前年齡</h2>
<p><code>Age</code> 標頭記錄的是：</p>
<blockquote>
<p>the cache&rsquo;s estimate of the number of seconds since the origin server generated or validated the response.（RFC 9111 §4.2.3）</p></blockquote>
<p>快取不經驗證、直接用保存的副本回應請求時，必須在回應裡帶上 <code>Age</code>，值是這份副本目前的年齡（RFC 9111 §4）。下游的快取收到帶 <code>Age</code> 的回應，會把這個值算進自己那份副本的年齡。規範給的保守算法還計入網路傳輸的延遲，結構是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">corrected_initial_age = max(收到時 Date 與自己時鐘的差, 上游給的 Age + 這次請求的往返時間)
</span></span><span class="line"><span class="ln">2</span><span class="cl">current_age = corrected_initial_age + 副本在自己這裡放了多久</span></span></code></pre></div><p>這個機制讓年齡沿著快取鏈累加，影響的是下游拿到的剩餘新鮮時間。一個回應帶 <code>Cache-Control: max-age=600</code>，在 CDN 上放了 500 秒之後被某個瀏覽器請求，CDN 回給瀏覽器的回應帶 <code>Age: 500</code>，瀏覽器算出的剩餘新鮮時間就只有 100 秒，不是 600 秒。origin 寫的 <code>max-age</code> 是從 origin 產生回應起算的總期限，不是每一個快取各自重新計時。</p>
<p>送不送 <code>Age</code>，實作之間就有差異。實測時 Varnish 7.7 每次由快取回應都送出遞增的 <code>Age</code>；nginx 1.27 的 proxy_cache 不送 <code>Age</code>，而且把 <code>Date</code> 換成送出當下的時間。下游原本可以用 <code>Date</code> 與自己時鐘的差估出副本的年齡；<code>Age</code> 沒送、<code>Date</code> 又被換成送出當下的時間，下游（例如接在 nginx 外面的 CDN）就估不出來，因此會把副本多用一段時間，長度等於副本在 nginx 裡已經放了的時間。規範也提醒，回應裡沒有 <code>Age</code> 不代表它剛從 origin 拿來（RFC 9111 §5.1）。</p>
<h2 id="沒有明確期限時的啟發式期限">沒有明確期限時的啟發式期限</h2>
<p>回應沒有 <code>s-maxage</code>、<code>max-age</code>、<code>Expires</code> 時，規範允許快取自己估一個期限，稱為<strong>啟發式期限</strong>（heuristic freshness）。允許的範圍有兩個限制（RFC 9111 §4.2.2）：</p>
<ul>
<li>只能用在<strong>狀態碼屬於可啟發式快取的回應</strong>上，或回應用 <code>public</code> 這類指令明確標成可快取。</li>
<li>回應有明確期限時，不可以使用啟發式期限。</li>
</ul>
<p>可啟發式快取的狀態碼由 RFC 9110 列舉：</p>
<blockquote>
<p>200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, and 501（RFC 9110 §15.1）</p></blockquote>
<p>302 與 307 不在這份可啟發式快取的清單裡，規範不允許快取保存沒有快取標頭的 302、307（<a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a> 的〈可以被保存的回應〉）；實測的 Varnish 7.7 與 nginx 1.27 都照做，但部分 CDN 的文件記載會替沒有快取標頭的轉址套用自己的預設保存時間，以各家文件為準。301 與 308 在清單裡，沒有快取標頭時可以被保存，能直接使用多久由快取自己估算。</p>
<p>規範沒有規定估算的演算法，只建議一個上限的參考做法：以回應的 <code>Last-Modified</code> 為起點，期限不超過「從那個時間點到現在」這段間隔的某個比例。</p>
<blockquote>
<p>caches are encouraged to use a heuristic expiration value that is no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%.（RFC 9111 §4.2.2）</p></blockquote>
<p>措辭是「encouraged」與「might」，10% 是例子，不是規定。一個在一年前修改過的檔案，照 10% 估算的新鮮度期限約 36 天；一個剛修改過的檔案，期限就很短。這個做法的假設是：很久沒改的內容，短期內也不太會改。</p>
<h2 id="各實作的啟發式期限">各實作的啟發式期限</h2>
<p>因為規範沒有規定演算法，各實作的做法差很多。以下是 2026-10-02 讀原始碼與實測的結果，版本會變，套用到自己的環境前要查當下的文件或原始碼：</p>
<table>
  <thead>
      <tr>
          <th>實作</th>
          <th>啟發式期限的做法</th>
          <th>資料來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Chromium</td>
          <td>200、203、206 用 <code>Last-Modified</code> 間隔的 10%，不設上限，沒有 <code>Last-Modified</code> 時為 0；300、301、308、410 沒有明確期限時<strong>永不過期</strong>；其他狀態碼為 0</td>
          <td><code>net/http/http_response_headers.cc</code> 的 <code>GetFreshnessLifetimes</code></td>
      </tr>
      <tr>
          <td>Firefox</td>
          <td>用 <code>Last-Modified</code> 間隔的 10%，上限一週；410 永不過期</td>
          <td><code>netwerk/protocol/http/nsHttpResponseHead.cpp</code> 的 <code>ComputeFreshnessLifetime</code></td>
      </tr>
      <tr>
          <td>Varnish 7.7</td>
          <td>不看 <code>Last-Modified</code>，沒有明確期限時套用自己的預設存活時間（參數 <code>default_ttl</code>）；只有 <code>Last-Modified</code> 的回應與完全沒有標頭的回應，實測得到相同的期限</td>
          <td>實測與 <code>varnishlog</code> 的 TTL 紀錄</td>
      </tr>
      <tr>
          <td>nginx proxy_cache</td>
          <td>沒有明確期限、也沒有用 <code>proxy_cache_valid</code>（nginx 依狀態碼指定預設保存時間的設定）指定時不保存</td>
          <td>實測</td>
      </tr>
  </tbody>
</table>
<p>上面這張各實作的啟發式期限表，解釋了沒有快取標頭的 301 為什麼在各快取上的結果不同。這樣的 301 在 Chromium 上永不過期，副本只要還在快取裡，就不會再向 origin 確認：實測時先讓 Chrome 跟著 <code>/r301</code> 轉到 <code>/target-a</code>，再把 origin 的轉址目的地改成 <code>/target-b</code>，Chrome 再次前往 <code>/r301</code> 時沒有送出請求，直接去了 <code>/target-a</code>。Chrome 不再送出請求，origin 的存取紀錄裡也就看不到這些使用者，這類問題只會從使用者回報裡出現。同一個回應在 Varnish 上只在預設存活時間內直接使用，在 nginx 上完全不保存。同一組標頭在三個快取上得到三種結果，原因是這個回應把新鮮度期限交給了各實作自己估。</p>
<p>所以內容可能改變的回應，要明寫新鮮度期限：要被保存就寫 <code>max-age</code>（以及需要時的 <code>s-maxage</code>），不要被保存就寫 <code>no-store</code>。不寫等於把期限交給每一個經過的快取各自決定。</p>
<h2 id="新鮮度期限與實際保存時間">新鮮度期限與實際保存時間</h2>
<p>新鮮度期限規定的是副本<strong>最多</strong>可以被直接使用多久，不保證快取會保存那麼久。規範把快取定位為選用功能：</p>
<blockquote>
<p>Although caching is an entirely OPTIONAL feature of HTTP（RFC 9111 §2）</p></blockquote>
<p>規範要求的是哪些回應不能存、不能重用，不要求快取把副本存到期滿。快取空間滿了、副本太少被用到、快取重啟，都會讓副本提早消失。所以 <code>max-age</code> 不能拿來當「這段時間內 origin 一定不會收到請求」的保證：容量規劃時 origin 要能承受副本提早消失、快取命中率下降時多出來的回源流量：容量怎麼估見 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>；命中率掉下來時擋住回源尖峰的手段見 <a href="/blog/backend/knowledge-cards/origin-protection/" data-link-title="Origin Protection" data-link-desc="快取失效或冷啟動時、miss 流量集中打回正式來源會不會壓垮它——回源保護的手段組合與監控面">Origin Protection</a> 卡。</p>
<p>新鮮度期限也只約束快取，不約束瀏覽器怎麼顯示畫面：</p>
<blockquote>
<p>freshness applies only to cache operation; it cannot be used to force a user agent to refresh its display or reload a resource.（RFC 9111 §4.2）</p></blockquote>
<p>使用者按上一頁看到的舊畫面，屬於瀏覽器的歷史機制，不受新鮮度期限約束，見 <a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a>。</p>
]]></content:encoded></item><item><title>12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</title><link>https://tarrragon.github.io/blog/backend/12-http-caching/revalidation/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/12-http-caching/revalidation/</guid><description>&lt;p>這篇整理一份副本過期之後，快取怎麼向 origin 確認它還能不能用。新鮮度期限怎麼算見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限&lt;/a>；在某些條件下不驗證就直接給出過期副本，是 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error&lt;/a> 的範圍。&lt;/p>
&lt;p>副本過期之後仍可以繼續使用，條件是先向 origin 確認：快取帶著副本的版本識別去問 origin，如果 origin 端的內容沒變，origin 只回一個沒有內容的 304，快取更新副本的標頭後繼續使用原本的內容。規範把這個過程稱為&lt;strong>驗證&lt;/strong>（validating 或 revalidating，RFC 9111 §4.3）。驗證省下的是傳輸內容的頻寬與 origin 產生內容的成本，請求本身仍然要送到 origin。&lt;/p>
&lt;h2 id="驗證子etag-與-last-modified">驗證子：ETag 與 Last-Modified&lt;/h2>
&lt;p>拿來比對版本的值稱為&lt;strong>驗證子&lt;/strong>（validator），有兩種：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>ETag&lt;/code>&lt;/strong>：origin 替內容的每個版本給的識別字串，例如 &lt;code>ETag: &amp;quot;v1&amp;quot;&lt;/code>。內容改變時 ETag 跟著改變。&lt;/li>
&lt;li>&lt;strong>&lt;code>Last-Modified&lt;/code>&lt;/strong>：內容最後修改的時間，例如 &lt;code>Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>&lt;code>ETag&lt;/code> 分強弱兩種。&lt;strong>強驗證子&lt;/strong>在內容的任何位元改變時都會改變；做不到這一點的（例如只在語意有變時才換值），origin 必須在值前面加 &lt;code>W/&lt;/code> 標成弱驗證子，例如 &lt;code>ETag: W/&amp;quot;v1&amp;quot;&lt;/code>（RFC 9110 §8.8.1、§8.8.3）。驗證快取副本時用的是弱比較，所以強弱 ETag 都能用來驗證（RFC 9110 §13.1.2）；強弱的差別影響的是分段請求（Range）這類需要逐位元一致的場合。&lt;/p>
&lt;p>&lt;code>Last-Modified&lt;/code> 的時間解析度只到秒。同一秒內改了兩次的內容，&lt;code>Last-Modified&lt;/code> 分不出來，這時要靠 &lt;code>ETag&lt;/code>（RFC 9110 §8.8.3）。origin 通常兩種都送，規範也要求快取送出驗證請求時，副本有 &lt;code>ETag&lt;/code> 就必須放進請求，有 &lt;code>Last-Modified&lt;/code> 通常也要放（RFC 9111 §4.3.1）。&lt;/p>
&lt;h2 id="條件請求與-304-回應">條件請求與 304 回應&lt;/h2>
&lt;p>快取驗證時送出的是&lt;strong>條件請求&lt;/strong>：在請求裡附上副本的驗證子，問 origin「這個版本還是最新的嗎」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">GET /res.js HTTP/1.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">If-None-Match: &amp;#34;v1&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>If-None-Match&lt;/code> 帶副本的 &lt;code>ETag&lt;/code>。origin 端目前的 ETag 與它相符（弱比較），代表內容沒變。&lt;/li>
&lt;li>&lt;code>If-Modified-Since&lt;/code> 帶副本的 &lt;code>Last-Modified&lt;/code>。origin 端內容的修改時間早於或等於這個時間，代表內容沒變。&lt;/li>
&lt;li>兩個都有時，origin 只看 &lt;code>If-None-Match&lt;/code>，忽略 &lt;code>If-Modified-Since&lt;/code>（RFC 9110 §13.1.3）。&lt;/li>
&lt;/ul>
&lt;p>內容沒變時，origin 對 GET 與 HEAD 回 &lt;strong>304 Not Modified&lt;/strong>；內容變了就回 200 與新內容（RFC 9110 §13.1.2）。304 沒有內容，但必須帶上 200 回應會帶的 &lt;code>Cache-Control&lt;/code>、&lt;code>Expires&lt;/code>、&lt;code>ETag&lt;/code>、&lt;code>Date&lt;/code>、&lt;code>Vary&lt;/code> 等標頭（RFC 9110 §15.4.5）。快取收到 304 之後，用這些標頭更新副本，副本因此重新變新鮮，新的期限照 304 帶來的 &lt;code>Cache-Control&lt;/code> 計算（RFC 9111 §4.3.3、§4.3.4）。&lt;/p>
&lt;p>所以 origin 修改一個網址回應的 &lt;code>Cache-Control&lt;/code> 時，不必等快取把副本丟掉：下一次驗證時 origin 回的 304 帶著新的 &lt;code>Cache-Control&lt;/code>，快取把副本標頭裡的指令換成新的，內容仍沿用原本那一份。&lt;/p>
&lt;p>驗證時 origin 回 5xx，快取可以把錯誤轉給客戶端，也可以當成 origin 沒有回應（RFC 9111 §4.3.3）；當成沒有回應時，快取能在 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error&lt;/a> 說明的條件下延用過期副本。&lt;/p>
&lt;h2 id="過期副本在驗證前的處理">過期副本在驗證前的處理&lt;/h2>
&lt;p>規範的預設是過期副本不能直接給出：&lt;/p>
&lt;blockquote>
&lt;p>A cache MUST NOT generate a stale response unless it is disconnected or doing so is explicitly permitted by the client or origin server（RFC 9111 §4.2.4）&lt;/p></description><content:encoded><![CDATA[<p>這篇整理一份副本過期之後，快取怎麼向 origin 確認它還能不能用。新鮮度期限怎麼算見 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>；在某些條件下不驗證就直接給出過期副本，是 <a href="/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</a> 的範圍。</p>
<p>副本過期之後仍可以繼續使用，條件是先向 origin 確認：快取帶著副本的版本識別去問 origin，如果 origin 端的內容沒變，origin 只回一個沒有內容的 304，快取更新副本的標頭後繼續使用原本的內容。規範把這個過程稱為<strong>驗證</strong>（validating 或 revalidating，RFC 9111 §4.3）。驗證省下的是傳輸內容的頻寬與 origin 產生內容的成本，請求本身仍然要送到 origin。</p>
<h2 id="驗證子etag-與-last-modified">驗證子：ETag 與 Last-Modified</h2>
<p>拿來比對版本的值稱為<strong>驗證子</strong>（validator），有兩種：</p>
<ul>
<li><strong><code>ETag</code></strong>：origin 替內容的每個版本給的識別字串，例如 <code>ETag: &quot;v1&quot;</code>。內容改變時 ETag 跟著改變。</li>
<li><strong><code>Last-Modified</code></strong>：內容最後修改的時間，例如 <code>Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT</code>。</li>
</ul>
<p><code>ETag</code> 分強弱兩種。<strong>強驗證子</strong>在內容的任何位元改變時都會改變；做不到這一點的（例如只在語意有變時才換值），origin 必須在值前面加 <code>W/</code> 標成弱驗證子，例如 <code>ETag: W/&quot;v1&quot;</code>（RFC 9110 §8.8.1、§8.8.3）。驗證快取副本時用的是弱比較，所以強弱 ETag 都能用來驗證（RFC 9110 §13.1.2）；強弱的差別影響的是分段請求（Range）這類需要逐位元一致的場合。</p>
<p><code>Last-Modified</code> 的時間解析度只到秒。同一秒內改了兩次的內容，<code>Last-Modified</code> 分不出來，這時要靠 <code>ETag</code>（RFC 9110 §8.8.3）。origin 通常兩種都送，規範也要求快取送出驗證請求時，副本有 <code>ETag</code> 就必須放進請求，有 <code>Last-Modified</code> 通常也要放（RFC 9111 §4.3.1）。</p>
<h2 id="條件請求與-304-回應">條件請求與 304 回應</h2>
<p>快取驗證時送出的是<strong>條件請求</strong>：在請求裡附上副本的驗證子，問 origin「這個版本還是最新的嗎」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">GET /res.js HTTP/1.1
</span></span><span class="line"><span class="ln">2</span><span class="cl">If-None-Match: &#34;v1&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT</span></span></code></pre></div><ul>
<li><code>If-None-Match</code> 帶副本的 <code>ETag</code>。origin 端目前的 ETag 與它相符（弱比較），代表內容沒變。</li>
<li><code>If-Modified-Since</code> 帶副本的 <code>Last-Modified</code>。origin 端內容的修改時間早於或等於這個時間，代表內容沒變。</li>
<li>兩個都有時，origin 只看 <code>If-None-Match</code>，忽略 <code>If-Modified-Since</code>（RFC 9110 §13.1.3）。</li>
</ul>
<p>內容沒變時，origin 對 GET 與 HEAD 回 <strong>304 Not Modified</strong>；內容變了就回 200 與新內容（RFC 9110 §13.1.2）。304 沒有內容，但必須帶上 200 回應會帶的 <code>Cache-Control</code>、<code>Expires</code>、<code>ETag</code>、<code>Date</code>、<code>Vary</code> 等標頭（RFC 9110 §15.4.5）。快取收到 304 之後，用這些標頭更新副本，副本因此重新變新鮮，新的期限照 304 帶來的 <code>Cache-Control</code> 計算（RFC 9111 §4.3.3、§4.3.4）。</p>
<p>所以 origin 修改一個網址回應的 <code>Cache-Control</code> 時，不必等快取把副本丟掉：下一次驗證時 origin 回的 304 帶著新的 <code>Cache-Control</code>，快取把副本標頭裡的指令換成新的，內容仍沿用原本那一份。</p>
<p>驗證時 origin 回 5xx，快取可以把錯誤轉給客戶端，也可以當成 origin 沒有回應（RFC 9111 §4.3.3）；當成沒有回應時，快取能在 <a href="/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</a> 說明的條件下延用過期副本。</p>
<h2 id="過期副本在驗證前的處理">過期副本在驗證前的處理</h2>
<p>規範的預設是過期副本不能直接給出：</p>
<blockquote>
<p>A cache MUST NOT generate a stale response unless it is disconnected or doing so is explicitly permitted by the client or origin server（RFC 9111 §4.2.4）</p></blockquote>
<p>也就是只有兩種情形可以給出過期副本：快取連不上 origin，或 origin 與客戶端用指令明確允許（例如 <code>stale-while-revalidate</code>）。有些指令連這兩種情形也一併禁止：<code>no-cache</code> 與 <code>must-revalidate</code>（語意見下方〈no-cache、no-store 與 must-revalidate 的語意〉），以及只對共用快取有效的 <code>proxy-revalidate</code> 與 <code>s-maxage</code>（<a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a>）。</p>
<h2 id="no-cacheno-store-與-must-revalidate-的語意">no-cache、no-store 與 must-revalidate 的語意</h2>
<p>這三個回應指令在保存、新鮮期間內（副本目前年齡小於新鮮度期限的那段時間）的使用、過期後與連不上 origin 時各有規定：</p>
<table>
  <thead>
      <tr>
          <th>指令</th>
          <th>能不能保存</th>
          <th>新鮮期間內能不能直接使用</th>
          <th>過期後</th>
          <th>連不上 origin 時</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>no-store</code></td>
          <td>不能</td>
          <td>不適用（沒有副本）</td>
          <td>不適用（沒有副本）</td>
          <td>不適用（沒有副本）</td>
      </tr>
      <tr>
          <td><code>no-cache</code></td>
          <td>能</td>
          <td>不能，每次使用前都要驗證成功</td>
          <td>每次使用前都要驗證成功</td>
          <td>不能給出副本</td>
      </tr>
      <tr>
          <td><code>must-revalidate</code></td>
          <td>能</td>
          <td>能</td>
          <td>必須驗證成功才能使用</td>
          <td>回錯誤（建議 504），不能給出過期副本</td>
      </tr>
      <tr>
          <td>只有 <code>max-age</code></td>
          <td>能</td>
          <td>能</td>
          <td>驗證後使用</td>
          <td>可以給出過期副本</td>
      </tr>
  </tbody>
</table>
<p>規範原文：</p>
<ul>
<li><code>no-cache</code>：「the response MUST NOT be used to satisfy any other request without forwarding it for validation and receiving a successful response」（RFC 9111 §5.2.2.4）。<code>no-cache</code> 的回應可以保存，每次使用前都要驗證成功；名稱裡的「cache」指的是不經驗證就直接使用。</li>
<li><code>no-store</code>：「a cache MUST NOT store any part of either the immediate request or the response」（RFC 9111 §5.2.2.5）。它是唯一禁止所有快取保存的指令（<code>private</code> 只禁止共用快取保存）。規範同一節也寫明它不是可靠的隱私保證（「not a reliable or sufficient mechanism for ensuring privacy」）。另外，MDN 註明一個回應帶 <code>no-store</code>，不會刪掉快取裡同一個網址先前存下的副本。</li>
<li><code>must-revalidate</code>：新鮮期間內照常使用，過期之後一定要驗證；連不上 origin 時要回錯誤，不能拿過期副本頂替（RFC 9111 §5.2.2.2）。規範對它的用途說得很具體：「if and only if failure to validate a request could cause incorrect operation, such as a silently unexecuted financial transaction」。</li>
</ul>
<p>選擇的依據是兩個問題。第一，內容能不能留在任何快取裡，包括使用者自己的裝置：不能就是 <code>no-store</code>；只是不能讓別人拿到，用 <code>private</code> 就夠了（<a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a>），<code>no-store</code> 會連帶失去瀏覽器保存與條件請求省下的傳輸頻寬。第二，拿到舊內容的後果有多嚴重：每次都要確認是最新就是 <code>no-cache</code>；新鮮期間內可以接受、過期後即使連不上 origin 也不能拿舊內容頂替，就是 <code>must-revalidate</code>。</p>
<p>常見的遺留寫法是把幾個指令與舊標頭疊在一起，例如：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Cache-Control: no-cache, no-store, must-revalidate
</span></span><span class="line"><span class="ln">2</span><span class="cl">Pragma: no-cache
</span></span><span class="line"><span class="ln">3</span><span class="cl">Expires: 0</span></span></code></pre></div><p>這一組的實際效果由 <code>no-store</code> 決定：任何快取都不保存，<code>no-cache</code> 與 <code>must-revalidate</code> 只對保存下來的副本有意義，在這裡不起作用。<code>Expires: 0</code> 不是有效的日期，規範要求快取把無法解析的值、特別是 <code>0</code>，當成過去的時間處理（RFC 9111 §5.3）；這一組沒有 <code>max-age</code> 或 <code>s-maxage</code>，所以 <code>Expires</code> 不會被忽略，只認得 <code>Expires</code> 的舊快取會把這份回應當成一到就過期。<code>Pragma</code> 是 HTTP/1.0 時代給請求用的標頭，RFC 9111 §5.4 已將它廢止，並註明它在回應裡的意思從來沒有被定義，不能拿來代替回應的 <code>Cache-Control: no-cache</code>；規範沒有定義它在回應裡的作用，個別舊快取理不理會它無法預期。所以要的效果是「不保存」時，寫 <code>Cache-Control: no-store</code> 就夠了；要的是「可以保存、每次先確認」時，寫 <code>no-cache</code>，兩者不要疊在一起，否則 <code>no-store</code> 會讓 <code>no-cache</code> 想保留的條件請求省不到任何傳輸。</p>
<h2 id="共用快取實作的驗證預設">共用快取實作的驗證預設</h2>
<p>快取向 origin 送條件請求這一步，在實測的情境（<code>no-cache</code> 的回應，以及 Varnish 寬限時間過後、nginx 過期後的請求）裡，Varnish 7.7 與 nginx 1.27 預設設定下都沒有發生：</p>
<ul>
<li><strong><code>no-cache</code> 回應</strong>：兩者預設都不保存，比規範保守。規範允許保存 <code>no-cache</code> 回應，Varnish 自己的期限計算也判定可以保存；讓 Varnish 不保存的是內建設定（builtin VCL，VCL 是 Varnish 的設定語言）裡一條在回應沒有 <code>Surrogate-Control</code> 時，把 <code>no-cache|no-store|private</code> 一律標成不可快取的規則。結果是 Varnish 與 nginx 把每個請求都以一般的 GET 轉給 origin，不是條件請求，origin 每次都回完整的 200。也就是說，回應寫 <code>no-cache</code> 時，這一層反向代理的命中率是零，所有請求的負載都由 origin 承擔；容量規劃要把這類回應當成前面沒有快取來估（<a href="/blog/backend/knowledge-cards/origin-protection/" data-link-title="Origin Protection" data-link-desc="快取失效或冷啟動時、miss 流量集中打回正式來源會不會壓垮它——回源保護的手段組合與監控面">Origin Protection</a> 卡）。</li>
<li><strong>有 <code>max-age</code> 的回應過期之後</strong>：Varnish 預設在一段寬限時間（參數 <code>default_grace</code>）內先給出過期副本，同時在背景向 origin 抓新的（見 <a href="/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</a>）；依 Varnish 7.7 的原始碼，副本是帶 <code>ETag</code> 或 <code>Last-Modified</code> 的 200 回應、內容不是空的時，這次背景抓取會送條件請求；實測在 <code>max-age=5</code> 的副本過期後約 2 秒、仍在寬限時間內送出請求，客戶端拿到的是過期副本（回應的 <code>Age</code> 為 7、Varnish 回報命中），origin 端同時收到帶 <code>If-None-Match</code> 與 <code>If-Modified-Since</code> 的請求。寬限時間也過了之後，副本只在保留時間（參數 <code>default_keep</code>，預設 0）內留著供條件請求使用；預設設定下副本已經丟掉，實測在這個時間點的請求，Varnish 送給 origin 的是一般的 GET。nginx 預設不打開 <code>proxy_cache_revalidate</code>，過期後同步向 origin 送一般的 GET。把 Varnish 的 keep 設成大於 0（實測同時把 grace 設為 0、keep 設為 60 秒）、或打開 nginx 的 <code>proxy_cache_revalidate</code> 之後，Varnish 寬限時間過後的請求、以及 nginx 過期後的請求才改送帶 <code>If-None-Match</code> 與 <code>If-Modified-Since</code> 的條件請求，origin 回 304，快取繼續使用原本的內容。</li>
<li><strong>客戶端自己送條件請求、而快取裡的副本還新鮮</strong>：兩者都直接回 304 給客戶端，不轉送到 origin。這是規範允許的行為。</li>
</ul>
<p>這代表 origin 端設定好 <code>ETag</code> 與 <code>no-cache</code>，不保證共用快取會照規範去驗證。origin 有多台伺服器時，同一份內容在每一台算出的 <code>ETag</code> 也要相同，否則條件請求落到另一台就比對不上，只能回完整的 200。要讓驗證在共用快取上發生，要另外查手上那個快取的設定：Varnish 的 keep 與內建設定的覆寫方式，nginx 的 <code>proxy_cache_revalidate</code>，CDN 則查各家文件裡「revalidation」或「conditional request」相關的說明。</p>
<p>瀏覽器上的驗證什麼時候發生，跟使用者是重新整理、強制重新整理還是一般瀏覽有關，見 <a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a>。</p>
]]></content:encoded></item><item><title>12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</title><link>https://tarrragon.github.io/blog/backend/12-http-caching/cache-key/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/12-http-caching/cache-key/</guid><description>&lt;p>這篇整理快取收到一個請求時，怎麼決定要拿哪一份保存的副本回應它，或判定沒有可用的副本。這個判斷決定了一份副本會被哪些人拿到，所以也是個人化內容外洩到別人手上的成因所在。保存位置的分類見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取&lt;/a>。本篇談的外洩發生在共用快取上：同一份副本被發給不同使用者。另一種「看到別人的畫面」發生在同一台裝置上，前一個使用者的頁面在按上一頁時被瀏覽器的 back/forward cache 還原，見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁&lt;/a> 的〈上一頁與 back/forward cache〉。&lt;/p>
&lt;h2 id="快取鍵的組成">快取鍵的組成&lt;/h2>
&lt;p>快取用來挑選副本的資訊稱為&lt;strong>快取鍵&lt;/strong>（cache key）。規範定義的最小組成是請求方法加上目標網址：&lt;/p>
&lt;blockquote>
&lt;p>The &amp;ldquo;cache key&amp;rdquo; is the information a cache uses to choose a response and is composed from, at a minimum, the request method and target URI（RFC 9111 §2）&lt;/p>&lt;/blockquote>
&lt;p>規範也指出，許多常用的快取只保存 GET 的回應，所以只用網址當快取鍵。網址包含查詢字串，&lt;code>/products?id=1&lt;/code> 與 &lt;code>/products?id=1&amp;amp;utm_source=mail&lt;/code> 是兩個不同的快取鍵，各自保存一份副本。行銷追蹤參數這類不影響內容的查詢字串，會讓同一份內容被拆成很多份副本、降低命中率；CDN 通常提供忽略特定參數的設定，名稱與用法查各家文件。&lt;/p>
&lt;p>方法方面，規範定義了 GET、HEAD、POST 三種方法的快取語意，而 PUT、DELETE 這類方法的回應不能快取（RFC 9110 §9.2.3、§9.3）。POST 的回應只有在帶明確期限、而且帶一個值和請求網址相同的 &lt;code>Content-Location&lt;/code> 時才能保存（RFC 9110 §9.3.3）；規範也指出，絕大多數實作只支援 GET 與 HEAD（「the overwhelming majority of cache implementations only support GET and HEAD」，RFC 9110 §9.2.3）。&lt;/p>
&lt;h2 id="vary-標頭加進快取鍵的請求標頭">Vary 標頭加進快取鍵的請求標頭&lt;/h2>
&lt;p>同一個網址可能依請求標頭回不同內容：依 &lt;code>Accept-Encoding&lt;/code> 回 gzip 或 brotli 壓縮的版本，依 &lt;code>Accept-Language&lt;/code> 回不同語言。origin 用 &lt;code>Vary&lt;/code> 標頭告訴快取：這個回應的內容跟哪些請求標頭有關，挑副本時這些標頭也要相符。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Vary: Accept-Encoding&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>In other words, Vary expands the cache key required to match a new request to the stored cache entry.（RFC 9110 §12.5.5）&lt;/p>&lt;/blockquote>
&lt;p>比對規則有幾個細節（RFC 9111 §4.1）：&lt;/p>
&lt;ul>
&lt;li>快取比對新請求與副本當初那個請求在 &lt;code>Vary&lt;/code> 列出的標頭值時，只允許去掉空白、合併同名標頭這類不改變語意的正規化，其餘要逐字相符。&lt;/li>
&lt;li>請求沒有帶某個 &lt;code>Vary&lt;/code> 列出的標頭時，只能和同樣沒有帶這個標頭的副本相符。&lt;/li>
&lt;li>&lt;code>Vary: *&lt;/code> 永遠不相符，等於這個回應每次被重用前都要先向 origin 驗證。&lt;/li>
&lt;/ul>
&lt;p>&lt;code>Vary&lt;/code> 列出的標頭值越分散，副本就越多。實測時同一個網址回 &lt;code>Vary: Accept-Encoding&lt;/code>，nginx 1.27 依請求標頭的原始字串分開保存：&lt;code>gzip&lt;/code>、&lt;code>br&lt;/code>、&lt;code>gzip, deflate, br&lt;/code> 與沒帶這個標頭各存一份。Varnish 7.7 在預設設定下會先把 &lt;code>Accept-Encoding&lt;/code> 正規化、向 origin 一律要 gzip、再自己替客戶端解壓，所以四種請求共用同一份副本。這是實作的處理，不是規範的要求；所用的快取產品怎麼處理 &lt;code>Vary&lt;/code>，要查它的文件。&lt;/p>
&lt;h2 id="帶-authorization-的請求在共用快取的重用規則">帶 Authorization 的請求在共用快取的重用規則&lt;/h2>
&lt;p>請求帶 &lt;code>Authorization&lt;/code> 標頭，代表回應可能只屬於那個使用者。規範對共用快取的規則是：除非回應用指令明確允許，否則不能把這種回應拿去回應其他請求。&lt;/p>
&lt;blockquote>
&lt;p>A shared cache MUST NOT use a cached response to a request with an Authorization header field &amp;hellip; to satisfy any subsequent request unless the response contains a Cache-Control field with a response directive &amp;hellip; that allows it to be stored by a shared cache（RFC 9111 §3.5）&lt;/p></description><content:encoded><![CDATA[<p>這篇整理快取收到一個請求時，怎麼決定要拿哪一份保存的副本回應它，或判定沒有可用的副本。這個判斷決定了一份副本會被哪些人拿到，所以也是個人化內容外洩到別人手上的成因所在。保存位置的分類見 <a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a>。本篇談的外洩發生在共用快取上：同一份副本被發給不同使用者。另一種「看到別人的畫面」發生在同一台裝置上，前一個使用者的頁面在按上一頁時被瀏覽器的 back/forward cache 還原，見 <a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a> 的〈上一頁與 back/forward cache〉。</p>
<h2 id="快取鍵的組成">快取鍵的組成</h2>
<p>快取用來挑選副本的資訊稱為<strong>快取鍵</strong>（cache key）。規範定義的最小組成是請求方法加上目標網址：</p>
<blockquote>
<p>The &ldquo;cache key&rdquo; is the information a cache uses to choose a response and is composed from, at a minimum, the request method and target URI（RFC 9111 §2）</p></blockquote>
<p>規範也指出，許多常用的快取只保存 GET 的回應，所以只用網址當快取鍵。網址包含查詢字串，<code>/products?id=1</code> 與 <code>/products?id=1&amp;utm_source=mail</code> 是兩個不同的快取鍵，各自保存一份副本。行銷追蹤參數這類不影響內容的查詢字串，會讓同一份內容被拆成很多份副本、降低命中率；CDN 通常提供忽略特定參數的設定，名稱與用法查各家文件。</p>
<p>方法方面，規範定義了 GET、HEAD、POST 三種方法的快取語意，而 PUT、DELETE 這類方法的回應不能快取（RFC 9110 §9.2.3、§9.3）。POST 的回應只有在帶明確期限、而且帶一個值和請求網址相同的 <code>Content-Location</code> 時才能保存（RFC 9110 §9.3.3）；規範也指出，絕大多數實作只支援 GET 與 HEAD（「the overwhelming majority of cache implementations only support GET and HEAD」，RFC 9110 §9.2.3）。</p>
<h2 id="vary-標頭加進快取鍵的請求標頭">Vary 標頭加進快取鍵的請求標頭</h2>
<p>同一個網址可能依請求標頭回不同內容：依 <code>Accept-Encoding</code> 回 gzip 或 brotli 壓縮的版本，依 <code>Accept-Language</code> 回不同語言。origin 用 <code>Vary</code> 標頭告訴快取：這個回應的內容跟哪些請求標頭有關，挑副本時這些標頭也要相符。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Vary: Accept-Encoding</span></span></code></pre></div><blockquote>
<p>In other words, Vary expands the cache key required to match a new request to the stored cache entry.（RFC 9110 §12.5.5）</p></blockquote>
<p>比對規則有幾個細節（RFC 9111 §4.1）：</p>
<ul>
<li>快取比對新請求與副本當初那個請求在 <code>Vary</code> 列出的標頭值時，只允許去掉空白、合併同名標頭這類不改變語意的正規化，其餘要逐字相符。</li>
<li>請求沒有帶某個 <code>Vary</code> 列出的標頭時，只能和同樣沒有帶這個標頭的副本相符。</li>
<li><code>Vary: *</code> 永遠不相符，等於這個回應每次被重用前都要先向 origin 驗證。</li>
</ul>
<p><code>Vary</code> 列出的標頭值越分散，副本就越多。實測時同一個網址回 <code>Vary: Accept-Encoding</code>，nginx 1.27 依請求標頭的原始字串分開保存：<code>gzip</code>、<code>br</code>、<code>gzip, deflate, br</code> 與沒帶這個標頭各存一份。Varnish 7.7 在預設設定下會先把 <code>Accept-Encoding</code> 正規化、向 origin 一律要 gzip、再自己替客戶端解壓，所以四種請求共用同一份副本。這是實作的處理，不是規範的要求；所用的快取產品怎麼處理 <code>Vary</code>，要查它的文件。</p>
<h2 id="帶-authorization-的請求在共用快取的重用規則">帶 Authorization 的請求在共用快取的重用規則</h2>
<p>請求帶 <code>Authorization</code> 標頭，代表回應可能只屬於那個使用者。規範對共用快取的規則是：除非回應用指令明確允許，否則不能把這種回應拿去回應其他請求。</p>
<blockquote>
<p>A shared cache MUST NOT use a cached response to a request with an Authorization header field &hellip; to satisfy any subsequent request unless the response contains a Cache-Control field with a response directive &hellip; that allows it to be stored by a shared cache（RFC 9111 §3.5）</p></blockquote>
<p>明確允許的指令是 <code>public</code>、<code>s-maxage</code> 與 <code>must-revalidate</code>。所以帶 <code>Authorization</code> 的請求，回應只寫 <code>max-age=60</code> 時，共用快取不能拿它回應別人；寫成 <code>public, max-age=60</code> 時可以，而且可以拿去回應任何請求，包括沒有帶 <code>Authorization</code> 的匿名請求：共用快取不替 origin 檢查權限。只對登入者開放、但每個登入者內容都相同的回應，寫 <code>public</code> 等於對匿名使用者公開，仍要寫 <code>private</code>，或靠 CDN 自己在邊緣驗證身分。<code>Vary</code> 不必列 <code>Authorization</code>，因為 RFC 9111 §3.5 這條規則已經禁止跨使用者重用（RFC 9110 §12.5.5）。</p>
<p>實測兩個共用快取在 RFC 9111 §3.5 這條規則上的表現完全相反：</p>
<ul>
<li><strong>Varnish 7.7</strong>：只要請求帶 <code>Authorization</code>，內建設定就不查快取、直接轉給 origin，連回應寫了 <code>public</code> 或 <code>s-maxage</code> 也一樣。這比規範保守。</li>
<li><strong>nginx 1.27 的 proxy_cache</strong>（預設設定）：使用者甲帶 <code>Authorization: Bearer t1</code> 請求一個只寫 <code>max-age=60</code> 的路徑，回應被保存下來；接著使用者乙帶 <code>Bearer t2</code>、以及完全沒帶 <code>Authorization</code> 的請求，都拿到了使用者甲的那份回應。這違反 RFC 9111 §3.5 的規則。</li>
</ul>
<p>nginx 的行為代表：origin 依靠「請求帶了 <code>Authorization</code>，快取就不會共用」這條規則來保護個人資料，在預設設定的 nginx 前面失效。開發與測試時多半只用一個帳號打同一個路徑，拿到的永遠是自己的資料，這種共用因此不容易在上線前被看到。需要登入才能拿到的回應，要在 origin 端明寫 <code>private</code> 或 <code>no-store</code>，不依賴快取對請求裡 <code>Authorization</code> 標頭的判斷。改標頭只約束之後產生的回應；已經存進共用快取的那份副本仍照它當初的標頭運作，到期之前會繼續發給別人，要讓它立刻停止，得在副本所在的快取上清除，CDN 與反向代理的清除方式見 <a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 的〈purge〉。</p>
<h2 id="cookie-與-set-cookie-在共用快取的處理">Cookie 與 Set-Cookie 在共用快取的處理</h2>
<p>以 Cookie 識別使用者的服務，規範沒有對請求的 <code>Cookie</code> 標頭訂任何快取規則；它只是一個普通的請求標頭，除非 <code>Vary: Cookie</code>，否則不進快取鍵。回應的 <code>Set-Cookie</code> 也不會阻止保存：</p>
<blockquote>
<p>Note that the Set-Cookie response header field [COOKIE] does not inhibit caching; a cacheable response with a Set-Cookie header field can be (and often is) used to satisfy subsequent requests to caches.（RFC 9111 §7.3）</p></blockquote>
<p>規範允許共用快取把一個帶 <code>Set-Cookie: session=使用者甲的值</code> 的回應保存下來，再原封不動地回應給使用者乙，原文也寫明這種事常常發生（「can be (and often is)」）；這時使用者乙的瀏覽器就收到了使用者甲的 session。規範在同一段建議 origin 自己用 <code>Cache-Control</code> 控制這類回應。</p>
<p>實作多半對帶 <code>Cookie</code> 的請求與帶 <code>Set-Cookie</code> 的回應加了保護，但做法不同，實測結果：</p>
<ul>
<li><strong>Varnish 7.7</strong>：請求帶 <code>Cookie</code> 時內建設定一律不查快取；回應帶 <code>Set-Cookie</code> 時內建設定不保存。</li>
<li><strong>nginx 1.27</strong>：回應帶 <code>Set-Cookie</code> 時預設不保存；請求的 <code>Cookie</code> 不影響快取鍵，回應有 <code>Vary: Cookie</code> 時才依 Cookie 值分開保存。</li>
</ul>
<p>這些保護是實作的預設，可以被設定關掉，CDN 的預設也各不相同。</p>
<h2 id="個人化回應的標頭選擇">個人化回應的標頭選擇</h2>
<p>回應的內容因使用者而不同時，標頭層面的做法如下（<code>private</code> 那一列成立的前提是回應沒有另外帶 <code>CDN-Cache-Control</code> 或 <code>Surrogate-Control</code>，或這些欄位裡也寫了 <code>private</code>、<code>no-store</code>（Varnish 的 <code>Surrogate-Control</code> 只認 <code>no-store</code>），見 <a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a> 的〈把指令指定給特定快取：CDN-Cache-Control〉）：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>效果</th>
          <th>適用情形</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Cache-Control: private</code></td>
          <td>共用快取不保存，瀏覽器可以保存</td>
          <td>個人化頁面、需登入的 API 回應，允許使用者自己的瀏覽器重複使用</td>
      </tr>
      <tr>
          <td><code>Cache-Control: no-store</code></td>
          <td>任何快取都不保存</td>
          <td>內容不能留在使用者裝置上（例如共用電腦上的敏感資料）</td>
      </tr>
      <tr>
          <td><code>Vary: Cookie</code></td>
          <td>共用快取依 Cookie 值分開保存</td>
          <td>很少適用：Cookie 值通常每個使用者都不同，副本數接近使用者數，命中率接近零</td>
      </tr>
  </tbody>
</table>
<p>標頭以外還有一種做法：頁面只有一小塊因使用者而不同（例如右上角的使用者名稱）時，把那一塊拆成另一個帶 <code>private</code> 的請求，頁面主體就能讓共用快取保存；前提是共用快取不會因為請求帶 Cookie 就跳過快取，Varnish 的內建設定會，要在 VCL 裡移除頁面主體請求的 Cookie。</p>
<p>MDN 的建議與表中 <code>private</code> 那一列一致：「you should specify Cache-Control: private instead of specifying a cookie for Vary.」個人化回應寫 <code>private</code>，是讓「這份回應能不能給別人」由 origin 決定，而不是交給共用快取的預設設定決定；〈帶 Authorization 的請求在共用快取的重用規則〉與〈Cookie 與 Set-Cookie 在共用快取的處理〉兩節的實測顯示，共用快取的預設在「會不會把回應發給別的使用者」這一點上，不同實作的結果相反。從外部確認某個網址有沒有把一個使用者的回應發給別人，做法在 <a href="/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面</a> 的〈檢查實際送出的快取標頭〉。</p>
<h2 id="沒有列進快取鍵的輸入與快取投毒">沒有列進快取鍵的輸入與快取投毒</h2>
<p>快取比對請求時只看快取鍵裡的部分。請求裡其他會改變回應內容的部分（某些請求標頭、某些查詢參數），PortSwigger 的研究稱為 unkeyed input，也就是沒有列進快取鍵的輸入。origin 依一個沒有列進快取鍵的輸入產生了不同的回應，而快取把這份回應存下來，在它過期之前，所有快取鍵相同的請求都會拿到它，不論那些請求有沒有帶那個輸入。攻擊者刻意利用這一點，送出帶惡意值的請求、讓有害的回應被存下來發給其他使用者，稱為 web cache poisoning（快取投毒）；James Kettle 在 2018 年 8 月發表於 PortSwigger 的研究〈Practical Web Cache Poisoning〉整理了這類實際案例。</p>
<p>一個常見的形態是框架讀取 <code>X-Forwarded-Host</code> 這類代理轉送用的請求標頭，拿它組出頁面裡的絕對網址。請求帶上攻擊者的網域時，回應裡的網址就指向攻擊者的伺服器；這個標頭不在快取鍵裡，被存下來的頁面就發給之後的每個使用者。這和本篇〈Vary 標頭加進快取鍵的請求標頭〉一節講的是同一件事的兩種結果：origin 依某個請求標頭改變回應時，那個標頭列進 <code>Vary</code>、或由快取設定加進快取鍵，不同的值就各存一份；沒有列進去，它就是一個沒有列進快取鍵的輸入，一個值產生的回應會被發給帶其他值的請求。</p>
<p>PortSwigger 列的防範裡，和快取鍵直接相關的有下面幾條：</p>
<ul>
<li>網站用不到的請求標頭，在快取或 origin 前停用，不讓框架讀到。這類標頭多半是代理或框架預設支援的，網站本身不需要。</li>
<li>為了命中率想把某個輸入排除在快取鍵外時，改成在快取前把請求改寫成正規化的值，讓同一個快取鍵只對應一種回應。</li>
<li>不接受帶 body 的 GET 請求，因為 GET 請求的 body 不在快取鍵裡，卻可能被 origin 讀取。</li>
</ul>
<p>其他還有把快取限縮在純靜態的回應、修補看似無法利用的前端漏洞，見研究原文。</p>
<h2 id="快取規則與-web-cache-deception">快取規則與 web cache deception</h2>
<p>web cache deception 和快取投毒是兩種不同的問題，PortSwigger 的文件也明確區分：快取投毒操弄的是快取鍵，讓攻擊者的有害回應被存下來發給別人；web cache deception 利用的是快取規則，讓受害者帶個人資料的回應被存下來，再被攻擊者取走。</p>
<p>CDN 常依副檔名（<code>.css</code>、<code>.js</code>）或目錄（<code>/static</code>）設定快取規則，把符合的網址當成可以保存的靜態檔，而快取與 origin 解析網址路徑的方式可能不同。攻擊者引誘已登入的使用者點一個在快取看來是靜態檔、在 origin 看來是個人頁面的網址，origin 回出帶個人資料的頁面，快取依規則存下它，攻擊者再請求同一個網址就拿到那份副本。這種情形下快取鍵本身沒有問題，出問題的是快取規則覆寫了 origin 對這份回應能不能保存的判斷。PortSwigger 的防範建議：</p>
<ul>
<li>動態回應明寫 <code>Cache-Control: no-store, private</code>。</li>
<li>CDN 的快取規則不覆寫 origin 的 <code>Cache-Control</code>。</li>
<li>啟用 CDN 比對回應的 <code>Content-Type</code> 與網址副檔名是否一致的保護。</li>
<li>確認 origin 與快取解析網址路徑的方式一致。</li>
</ul>
<h2 id="瀏覽器快取的分區">瀏覽器快取的分區</h2>
<p>瀏覽器上另有一層規範允許的做法：在快取鍵裡加入額外資訊。規範舉的例子是加入發出請求的網站：</p>
<blockquote>
<p>user agent caches might include the referring site&rsquo;s identity, thereby &ldquo;double keying&rdquo; the cache to avoid some privacy risks（RFC 9111 §2）</p></blockquote>
<p>WHATWG Fetch 規範把 HTTP 快取依「頂層網站」分區（Fetch 的 network partition key）：同一個第三方資源，在網站甲與網站乙上被載入時，各存一份，目的是防止網站透過「某個資源是否已經在快取裡」推測使用者造訪過哪些網站。它對 origin 的影響是：跨網站共用的資源（例如公共 CDN 上的函式庫），不能預期因為使用者在別的網站載入過而命中快取。各瀏覽器分區的確切方式查各自的文件。</p>
]]></content:encoded></item><item><title>12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</title><link>https://tarrragon.github.io/blog/backend/12-http-caching/updating-stored-responses/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/12-http-caching/updating-stored-responses/</guid><description>&lt;p>這篇整理一份回應已經被各處快取保存之後，origin 要讓它們改用新內容時有哪些手段，以及每一種手段能傳到哪幾種快取；本模組把一種更新手段能作用到哪幾層快取、以及每一層裡的哪些副本，稱為這個手段的&lt;strong>作用範圍&lt;/strong>。快取的種類與經營者見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取&lt;/a>。瀏覽器快取與第三方轉送代理沒有管理介面，origin 要改變它們的副本，都要等它們再向 origin 送出請求；簽約的 CDN 與自己經營的反向代理另有清除手段，見下方〈purge〉。&lt;/p>
&lt;h2 id="規範定義的失效">規範定義的失效&lt;/h2>
&lt;p>HTTP 規範裡唯一讓快取主動丟棄副本的機制，綁在不安全的請求方法（規範術語，指會改變伺服器狀態的方法，RFC 9110 §9.2.1）上。快取收到 POST、PUT、DELETE 這類方法的成功回應時，必須讓同一個網址的副本失效：&lt;/p>
&lt;blockquote>
&lt;p>A cache MUST invalidate the target URI &amp;hellip; when it receives a non-error status code in response to an unsafe request method（RFC 9111 §4.4）&lt;/p>&lt;/blockquote>
&lt;p>「失效」是把副本移除，或標成下次使用前必須驗證；「成功回應」指 2xx 與 3xx。回應裡 &lt;code>Location&lt;/code> 與 &lt;code>Content-Location&lt;/code> 指到的網址也可以一起失效，但必須和請求的網址屬於同一個來源（scheme、主機名稱與連接埠都相同；RFC 9110 稱為 origin，與本模組其他地方指源站伺服器的 origin 是兩件事）。&lt;/p>
&lt;p>這個機制的作用範圍很窄，規範自己寫明了：&lt;/p>
&lt;blockquote>
&lt;p>a state-changing request would only invalidate responses in the caches it travels through.（RFC 9111 §4.4）&lt;/p>&lt;/blockquote>
&lt;p>使用者甲送出一個 PUT 修改商品資料，被失效的只有使用者甲瀏覽器快取裡的副本，以及這個請求經過的那一個 CDN 節點上的副本。使用者乙的瀏覽器、其他 CDN 節點、使用者乙公司的轉送代理，都不會知道這次修改，各自繼續使用手上的副本直到過期。&lt;/p>
&lt;h2 id="purge">purge&lt;/h2>
&lt;p>&lt;strong>purge&lt;/strong> 指 origin 直接要求某個快取刪除指定的副本。HTTP 規範沒有定義 purge 這個操作：RFC 9110 與 RFC 9111 都沒有規定它，IANA 的 HTTP 方法登錄表也沒有 &lt;code>PURGE&lt;/code> 方法（2026-10-02 查閱）。MDN 對此的描述是：&lt;/p>
&lt;blockquote>
&lt;p>There is no way to delete responses on an intermediate server that have been stored with a long max-age.&lt;/p>&lt;/blockquote>
&lt;p>purge 因此是各個快取產品自己的功能，能用到什麼程度取決於經營者：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CDN&lt;/strong>：透過 CDN 的管理介面或 API 清除，可以依網址、依前綴或依標籤（tag，見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-tag-purge/" data-link-title="Cache Tag Purge" data-link-desc="CDN / cache 用 tag / surrogate key 批量失效多個關聯資源">Cache Tag Purge&lt;/a> 卡）清除。依標籤清除的操作模型見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源&lt;/a> 的〈Tag-based Purge 的操作模型〉。清除傳到所有節點需要時間，傳播時間與 API 呼叫的額度限制查各家文件。&lt;/li>
&lt;li>&lt;strong>自己經營的反向代理&lt;/strong>：Varnish 的內建設定把 &lt;code>PURGE&lt;/code> 當成不認得的方法，直接轉給 origin、不動快取，要在設定檔（VCL）裡自己寫處理規則；實測時寫了 &lt;code>PURGE&lt;/code>（刪除指定網址的副本）與 &lt;code>BAN&lt;/code>（依條件一次標記一批副本失效）兩種規則之後，兩者都能讓下一個請求回到 origin。其他產品是否支援、怎麼開啟，查各自的文件。&lt;/li>
&lt;li>&lt;strong>瀏覽器與第三方轉送代理&lt;/strong>：沒有 purge 管道。&lt;/li>
&lt;/ul>
&lt;p>purge 刪掉的副本不會留下可以延用的舊版，所以清除之後，下一批請求在被清掉的每一個節點上都對不到副本，同時回到 origin。清的是一個熱門網址、或一整個前綴與標籤時，這一批回源請求會在同一個時間點湧進 origin，形成 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">Cache Stampede&lt;/a> 描述的回源尖峰，清除的範圍越大、流量越高，尖峰越高。能壓低尖峰的做法都在清除的方式與回源的路徑上：把清除的範圍縮到真正改了的網址或標籤、避開流量高峰再清、讓 CDN 的 origin shield 或 request coalescing 把同時回源的請求合併成一次（&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源&lt;/a> 的〈Origin Protection 的設計責任〉）；部分 CDN 另外提供把副本標成過期而不刪除的清除方式，配合 &lt;code>stale-while-revalidate&lt;/code> 讓回源分散到背景進行，支援情形以各家文件為準。保護 origin 承受回源量的手段，見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/origin-protection/" data-link-title="Origin Protection" data-link-desc="快取失效或冷啟動時、miss 流量集中打回正式來源會不會壓垮它——回源保護的手段組合與監控面">Origin Protection&lt;/a> 卡。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理一份回應已經被各處快取保存之後，origin 要讓它們改用新內容時有哪些手段，以及每一種手段能傳到哪幾種快取；本模組把一種更新手段能作用到哪幾層快取、以及每一層裡的哪些副本，稱為這個手段的<strong>作用範圍</strong>。快取的種類與經營者見 <a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a>。瀏覽器快取與第三方轉送代理沒有管理介面，origin 要改變它們的副本，都要等它們再向 origin 送出請求；簽約的 CDN 與自己經營的反向代理另有清除手段，見下方〈purge〉。</p>
<h2 id="規範定義的失效">規範定義的失效</h2>
<p>HTTP 規範裡唯一讓快取主動丟棄副本的機制，綁在不安全的請求方法（規範術語，指會改變伺服器狀態的方法，RFC 9110 §9.2.1）上。快取收到 POST、PUT、DELETE 這類方法的成功回應時，必須讓同一個網址的副本失效：</p>
<blockquote>
<p>A cache MUST invalidate the target URI &hellip; when it receives a non-error status code in response to an unsafe request method（RFC 9111 §4.4）</p></blockquote>
<p>「失效」是把副本移除，或標成下次使用前必須驗證；「成功回應」指 2xx 與 3xx。回應裡 <code>Location</code> 與 <code>Content-Location</code> 指到的網址也可以一起失效，但必須和請求的網址屬於同一個來源（scheme、主機名稱與連接埠都相同；RFC 9110 稱為 origin，與本模組其他地方指源站伺服器的 origin 是兩件事）。</p>
<p>這個機制的作用範圍很窄，規範自己寫明了：</p>
<blockquote>
<p>a state-changing request would only invalidate responses in the caches it travels through.（RFC 9111 §4.4）</p></blockquote>
<p>使用者甲送出一個 PUT 修改商品資料，被失效的只有使用者甲瀏覽器快取裡的副本，以及這個請求經過的那一個 CDN 節點上的副本。使用者乙的瀏覽器、其他 CDN 節點、使用者乙公司的轉送代理，都不會知道這次修改，各自繼續使用手上的副本直到過期。</p>
<h2 id="purge">purge</h2>
<p><strong>purge</strong> 指 origin 直接要求某個快取刪除指定的副本。HTTP 規範沒有定義 purge 這個操作：RFC 9110 與 RFC 9111 都沒有規定它，IANA 的 HTTP 方法登錄表也沒有 <code>PURGE</code> 方法（2026-10-02 查閱）。MDN 對此的描述是：</p>
<blockquote>
<p>There is no way to delete responses on an intermediate server that have been stored with a long max-age.</p></blockquote>
<p>purge 因此是各個快取產品自己的功能，能用到什麼程度取決於經營者：</p>
<ul>
<li><strong>CDN</strong>：透過 CDN 的管理介面或 API 清除，可以依網址、依前綴或依標籤（tag，見 <a href="/blog/backend/knowledge-cards/cache-tag-purge/" data-link-title="Cache Tag Purge" data-link-desc="CDN / cache 用 tag / surrogate key 批量失效多個關聯資源">Cache Tag Purge</a> 卡）清除。依標籤清除的操作模型見 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a> 的〈Tag-based Purge 的操作模型〉。清除傳到所有節點需要時間，傳播時間與 API 呼叫的額度限制查各家文件。</li>
<li><strong>自己經營的反向代理</strong>：Varnish 的內建設定把 <code>PURGE</code> 當成不認得的方法，直接轉給 origin、不動快取，要在設定檔（VCL）裡自己寫處理規則；實測時寫了 <code>PURGE</code>（刪除指定網址的副本）與 <code>BAN</code>（依條件一次標記一批副本失效）兩種規則之後，兩者都能讓下一個請求回到 origin。其他產品是否支援、怎麼開啟，查各自的文件。</li>
<li><strong>瀏覽器與第三方轉送代理</strong>：沒有 purge 管道。</li>
</ul>
<p>purge 刪掉的副本不會留下可以延用的舊版，所以清除之後，下一批請求在被清掉的每一個節點上都對不到副本，同時回到 origin。清的是一個熱門網址、或一整個前綴與標籤時，這一批回源請求會在同一個時間點湧進 origin，形成 <a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">Cache Stampede</a> 描述的回源尖峰，清除的範圍越大、流量越高，尖峰越高。能壓低尖峰的做法都在清除的方式與回源的路徑上：把清除的範圍縮到真正改了的網址或標籤、避開流量高峰再清、讓 CDN 的 origin shield 或 request coalescing 把同時回源的請求合併成一次（<a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a> 的〈Origin Protection 的設計責任〉）；部分 CDN 另外提供把副本標成過期而不刪除的清除方式，配合 <code>stale-while-revalidate</code> 讓回源分散到背景進行，支援情形以各家文件為準。保護 origin 承受回源量的手段，見 <a href="/blog/backend/knowledge-cards/origin-protection/" data-link-title="Origin Protection" data-link-desc="快取失效或冷啟動時、miss 流量集中打回正式來源會不會壓垮它——回源保護的手段組合與監控面">Origin Protection</a> 卡。</p>
<h2 id="版本化網址與-immutable">版本化網址與 immutable</h2>
<p>既然已存的副本難以刪除，另一種做法是不去改它：內容一改就換一個網址。這個做法也稱 cache busting，最常見的形式是由前端建置工具在打包時，把內容的雜湊值寫進檔名：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">/static/app.3f9a1c.js</span></span></code></pre></div><p>內容改變時雜湊值跟著改變，網址也就跟著改變。新網址是一個新的快取鍵，所有快取都沒有它的副本，第一次請求一定會到 origin；舊網址的副本留在各處，重新載入過的頁面已經不再引用舊網址，這些副本等新鮮度期限過了、或快取空間不夠時自然被丟掉。已經開著的分頁仍可能延遲載入舊網址的程式片段，所以 origin 部署時舊檔要再保留一段時間，否則這些請求一落到沒有副本的快取，就會回到 origin 拿到 404。這個做法對每一層快取都有效，包括瀏覽器與轉送代理，因為它不需要任何快取配合刪除。</p>
<p>代價在引用端：引用這個資源的頁面必須跟著更新網址，而那個頁面本身就不能被長期快取，否則使用者拿到的舊頁面仍然引用舊網址。所以版本化網址要和引用它的頁面一起設定：頁面用短期限或 <code>no-cache</code>，頁面引用的靜態資源用版本化網址加長期限。</p>
<p>版本化網址的資源內容永遠不會改變，RFC 8246 為此定義了 <code>immutable</code> 指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Cache-Control: max-age=31536000, immutable</span></span></code></pre></div><p><code>31536000</code> 秒是一年，是常見的取值，不是規範規定的上限。</p>
<p>它告訴客戶端，新鮮期間內 origin 不會更新這個資源，客戶端在新鮮期間內不應該（SHOULD NOT）送條件請求確認，使用者按一般的重新整理時也不送（RFC 8246 §2）。幾個限制：使用者強制重新整理時仍然會重新抓取；規範建議客戶端只在 HTTPS 這類經過驗證的連線上採用 <code>immutable</code>（RFC 8246 §3）；不認得它的瀏覽器依 RFC 9111 忽略這個指令，照一般的 <code>max-age</code> 運作，各瀏覽器的支援情形查各自的文件。重新整理時瀏覽器送出什麼請求，見 <a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a>。</p>
<h2 id="clear-site-data">Clear-Site-Data</h2>
<p><code>Clear-Site-Data</code> 是 W3C 的一份規格，讓回應可以要求瀏覽器清除這個網站存在本機的資料，其中 <code>&quot;cache&quot;</code> 這個值清的是 HTTP 快取：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Clear-Site-Data: &#34;cache&#34;</span></span></code></pre></div><p>它有幾個限制，使它無法當成一般的更新手段：</p>
<ul>
<li><strong>只作用在瀏覽器</strong>。MDN 寫明它「has no effect on intermediate caches」，CDN 與轉送代理裡的副本不受影響。</li>
<li><strong>要瀏覽器對同一個主機送出請求、並在回應裡收到 <code>Clear-Site-Data</code> 才會生效</strong>。清除的範圍是主機名稱相同的整批快取條目，不限於收到標頭的那個網址。一個在 Chromium 上永不過期的 301（見 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>），使用者點它時不會連到 origin，這次點擊本身不會觸發清除；這份 301 副本清不清得掉，取決於使用者之後會不會造訪同一個主機的其他網址，這不是 origin 控制得了的。</li>
<li><strong>只在 HTTPS 上有效</strong>。</li>
<li><strong>規格狀態</strong>：W3C 正式發布的版本停在 2017 年的 Working Draft，Editor&rsquo;s Draft 最後更新於 2023 年，兩者都不是 Recommendation。各瀏覽器支援哪些值，查各自的文件。</li>
</ul>
<p>它適合的用途是使用者登出這類「從這個使用者的瀏覽器裡清掉資料」的時機，而不是改版時讓所有人拿到新內容。登出時通常不只清 <code>&quot;cache&quot;</code>，還會一起清 <code>&quot;cookies&quot;</code> 與 <code>&quot;storage&quot;</code>，各值在各瀏覽器的支援情形查 MDN 的 Clear-Site-Data 頁面。</p>
<h2 id="各種更新手段的作用範圍">各種更新手段的作用範圍</h2>
<table>
  <thead>
      <tr>
          <th>手段</th>
          <th>瀏覽器</th>
          <th>自己經營的反向代理</th>
          <th>簽約的 CDN</th>
          <th>第三方轉送代理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>規範的失效（不安全方法）</td>
          <td>只有發出請求的那個使用者</td>
          <td>只有那個請求經過的那一台</td>
          <td>只有請求經過的那個節點</td>
          <td>只有那個請求經過的那一台</td>
      </tr>
      <tr>
          <td>purge</td>
          <td>無</td>
          <td>依產品設定</td>
          <td>依 CDN 功能</td>
          <td>無</td>
      </tr>
      <tr>
          <td>版本化網址</td>
          <td>有效</td>
          <td>有效</td>
          <td>有效</td>
          <td>有效</td>
      </tr>
      <tr>
          <td>Clear-Site-Data</td>
          <td>只有收到這個回應的那個使用者</td>
          <td>無</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>等待到期</td>
          <td>有效</td>
          <td>有效</td>
          <td>有效</td>
          <td>有效</td>
      </tr>
      <tr>
          <td>下一次驗證（內容變了回 200、沒變回 304）</td>
          <td>驗證時更新</td>
          <td>驗證時更新</td>
          <td>驗證時更新</td>
          <td>驗證時更新</td>
      </tr>
  </tbody>
</table>
<h2 id="等待到期與改版全面生效的時間">等待到期與改版全面生效的時間</h2>
<p>表中「規範的失效」那一列是規範的要求：依 Varnish 7.7 的內建設定，POST、PUT、DELETE、PATCH 走 pass、直接轉給 origin；實測對一個副本已經存在 Varnish 7.7 與 nginx 1.27 proxy_cache 裡的網址送出 POST、origin 回 200 之後，同一網址的下一個 GET 在兩者上都仍由快取直接回應，兩者預設都不做這項失效。各家 CDN 是否實作，查各自的文件。</p>
<p>上表的「等待到期」與「下一次驗證」兩種手段對每一層都有效，但它們都要等：等副本過期，或等快取下一次回來驗證。所以一個網址的內容「改了之後最慢多久所有人都看得到新版」，有三個成分：</p>
<ul>
<li><strong>各層的新鮮度期限</strong>：回應寫了明確期限、而且每一層都正確傳遞 <code>Age</code> 時，這個成分的上限是各層裡最長的那個期限。回應沒有寫明確期限時，期限由各層自己估算，Chromium 對沒有快取標頭的 301 永不過期，這時就沒有上限（<a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>）。</li>
<li><strong>過期副本的延用</strong>：<code>stale-while-revalidate</code> 與 <code>stale-if-error</code> 的窗口（過期之後仍允許給出過期副本的秒數），以及 Varnish 這類實作自帶的寬限時間（參數 <code>default_grace</code>），會在期限之後再延長一段（<a href="/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</a>）。</li>
<li><strong>年齡沒有傳下去的層</strong>：這是各層新鮮度期限那一項的前提（每一層都正確傳遞 <code>Age</code>）不成立的情形。上游不送 <code>Age</code> 又改寫 <code>Date</code> 時，下游把收到的副本當成剛產生的，從零開始算它的期限；副本在上游已經放了的時間，會再加在下游的期限上，上限改成沿路各層期限相加（實測的 nginx proxy_cache 就是這樣，見 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a> 的〈Age 標頭與副本的目前年齡〉）。</li>
</ul>
<p>這三個成分只計 HTTP 快取：已經開著的分頁、back/forward cache 與 Service Worker 不受這些期限約束；也假設沒有任何一層用自己的設定覆寫 origin 的期限。</p>
<p>所以選各層的期限（<code>max-age</code>、<code>s-maxage</code>、<code>CDN-Cache-Control</code>）的值，同時也是在選內容改版時最慢多久能全面生效，而這個時間要估得出來，前提是回應寫了明確期限。業務上能容忍多久的舊資料，在 <a href="/blog/backend/knowledge-cards/freshness-window/" data-link-title="Freshness Window" data-link-desc="每類資料能容忍多久的不新鮮？依欄位風險分級定義過時窗口、取代單一全域 TTL">Freshness Window</a> 卡裡是一個分級的需求；<code>max-age</code> 是讓 HTTP 快取符合那個需求的手段。</p>
<p><a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">Cache Invalidation</a> 卡以應用層快取為主，討論 TTL、主動刪除、版本鍵、事件驅動等失效策略，和本篇的差別在快取由誰經營：應用層快取由服務自己讀寫，刪除一定做得到；本篇的快取多數不在 origin 手上。</p>
]]></content:encoded></item><item><title>12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</title><link>https://tarrragon.github.io/blog/backend/12-http-caching/serving-stale/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/12-http-caching/serving-stale/</guid><description>&lt;p>這篇整理快取可以不等驗證、直接給出已經過期的副本的條件。副本過期之後的一般處理（驗證）見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store&lt;/a>；本篇處理的是兩種例外：驗證可以在背景進行的時候，以及 origin 出錯的時候。新鮮度期限（副本不必詢問 origin 就能直接使用的時間）怎麼算，見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限&lt;/a>。&lt;/p>
&lt;h2 id="規範對過期副本的預設">規範對過期副本的預設&lt;/h2>
&lt;p>規範的預設是過期副本不能直接給出，只有兩種情形例外：快取連不上 origin，或 origin 與客戶端用指令明確允許（RFC 9111 §4.2.4）。&lt;code>no-cache&lt;/code>、&lt;code>must-revalidate&lt;/code>、&lt;code>proxy-revalidate&lt;/code> 與共用快取上的 &lt;code>s-maxage&lt;/code> 會連這兩種例外一起禁止，完整的規定見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store&lt;/a> 的〈過期副本在驗證前的處理〉。回應同時寫了 &lt;code>s-maxage&lt;/code> 與下面兩個指令時，兩個指令的情形不同。&lt;code>s-maxage&lt;/code> 在共用快取上禁止給出未經驗證的過期副本（RFC 9111 §4.2.4、§5.2.2.10）；§4.2.4 另一段列出的擴充指令許可，只在沒有這條禁止時才適用，所以 &lt;code>s-maxage&lt;/code> 與 &lt;code>stale-while-revalidate&lt;/code> 並用時，規範的答案是共用快取不延用過期副本。&lt;code>stale-if-error&lt;/code> 則不同：RFC 5861 §4 寫它可以「regardless of other freshness information」，與 RFC 9111 的禁止衝突，兩份規範之間沒有一致的答案。實作上，Varnish 7.7 的原始碼把 &lt;code>stale-while-revalidate&lt;/code> 的值直接當成寬限時間，不看 &lt;code>s-maxage&lt;/code>；CDN 怎麼處理這個組合，以各家文件與實測為準。&lt;/p>
&lt;p>明確允許的指令裡最常用的是 &lt;code>stale-while-revalidate&lt;/code> 與 &lt;code>stale-if-error&lt;/code>，兩者由 RFC 5861 定義。這份 RFC 的類別是 Informational，不在標準軌上（「This document is not an Internet Standards Track specification」），所以它的支援程度比 RFC 9111 的核心指令更依賴各實作。RFC 5861 寫到給出過期副本時要附 &lt;code>Warning&lt;/code> 標頭，而 &lt;code>Warning&lt;/code> 已在 RFC 9111 §5.5 廢止，實作多半不再送出它，不能拿它判斷回應是不是過期副本。&lt;/p>
&lt;h2 id="stale-while-revalidate">stale-while-revalidate&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Cache-Control: max-age=600, stale-while-revalidate=600&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這組標頭的意思是：新鮮度期限 600 秒；過期之後的 600 秒內，快取可以先把過期副本給出去，同時在背景向 origin 驗證或重新抓取，不讓請求等待（RFC 5861 §3）。過期之後允許給出過期副本的這段時間，本篇稱為 &lt;code>stale-while-revalidate&lt;/code> 的&lt;strong>窗口&lt;/strong>，長度就是指令後面的秒數。&lt;/p>
&lt;blockquote>
&lt;p>indicates that caches MAY serve the response in which it appears after it becomes stale, up to the indicated number of seconds.（RFC 5861 §3）&lt;/p>&lt;/blockquote>
&lt;p>&lt;code>stale-while-revalidate&lt;/code> 的使用條件：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>背景更新要靠請求觸發&lt;/strong>。只有在窗口內有請求進來，快取才會開始背景驗證；窗口內完全沒有請求，窗口結束後副本就照一般規則處理（RFC 5861 §3.1）。規範建議以請求觸發背景驗證，理由是避免放大攻擊（amplification attack，RFC 5861 §5）。&lt;/li>
&lt;li>&lt;strong>使用者可能拿到的最舊內容，年齡是 &lt;code>max-age&lt;/code> 加上 &lt;code>stale-while-revalidate&lt;/code> 的值&lt;/strong>。以 &lt;code>max-age=600, stale-while-revalidate=600&lt;/code> 為例，使用者最長可能拿到 1200 秒前產生的內容（RFC 5861 §3.1 的例子）。&lt;/li>
&lt;li>&lt;strong>窗口過後還沒更新成功，就不應該（SHOULD NOT）再給出過期副本&lt;/strong>，除非符合規範允許的另一種例外，例如連不上 origin（RFC 5861 §3）。&lt;/li>
&lt;/ul>
&lt;p>站內 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">Stale-While-Revalidate&lt;/a> 卡從使用者體驗與新鮮度的取捨描述同一個機制。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理快取可以不等驗證、直接給出已經過期的副本的條件。副本過期之後的一般處理（驗證）見 <a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a>；本篇處理的是兩種例外：驗證可以在背景進行的時候，以及 origin 出錯的時候。新鮮度期限（副本不必詢問 origin 就能直接使用的時間）怎麼算，見 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>。</p>
<h2 id="規範對過期副本的預設">規範對過期副本的預設</h2>
<p>規範的預設是過期副本不能直接給出，只有兩種情形例外：快取連不上 origin，或 origin 與客戶端用指令明確允許（RFC 9111 §4.2.4）。<code>no-cache</code>、<code>must-revalidate</code>、<code>proxy-revalidate</code> 與共用快取上的 <code>s-maxage</code> 會連這兩種例外一起禁止，完整的規定見 <a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a> 的〈過期副本在驗證前的處理〉。回應同時寫了 <code>s-maxage</code> 與下面兩個指令時，兩個指令的情形不同。<code>s-maxage</code> 在共用快取上禁止給出未經驗證的過期副本（RFC 9111 §4.2.4、§5.2.2.10）；§4.2.4 另一段列出的擴充指令許可，只在沒有這條禁止時才適用，所以 <code>s-maxage</code> 與 <code>stale-while-revalidate</code> 並用時，規範的答案是共用快取不延用過期副本。<code>stale-if-error</code> 則不同：RFC 5861 §4 寫它可以「regardless of other freshness information」，與 RFC 9111 的禁止衝突，兩份規範之間沒有一致的答案。實作上，Varnish 7.7 的原始碼把 <code>stale-while-revalidate</code> 的值直接當成寬限時間，不看 <code>s-maxage</code>；CDN 怎麼處理這個組合，以各家文件與實測為準。</p>
<p>明確允許的指令裡最常用的是 <code>stale-while-revalidate</code> 與 <code>stale-if-error</code>，兩者由 RFC 5861 定義。這份 RFC 的類別是 Informational，不在標準軌上（「This document is not an Internet Standards Track specification」），所以它的支援程度比 RFC 9111 的核心指令更依賴各實作。RFC 5861 寫到給出過期副本時要附 <code>Warning</code> 標頭，而 <code>Warning</code> 已在 RFC 9111 §5.5 廢止，實作多半不再送出它，不能拿它判斷回應是不是過期副本。</p>
<h2 id="stale-while-revalidate">stale-while-revalidate</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Cache-Control: max-age=600, stale-while-revalidate=600</span></span></code></pre></div><p>這組標頭的意思是：新鮮度期限 600 秒；過期之後的 600 秒內，快取可以先把過期副本給出去，同時在背景向 origin 驗證或重新抓取，不讓請求等待（RFC 5861 §3）。過期之後允許給出過期副本的這段時間，本篇稱為 <code>stale-while-revalidate</code> 的<strong>窗口</strong>，長度就是指令後面的秒數。</p>
<blockquote>
<p>indicates that caches MAY serve the response in which it appears after it becomes stale, up to the indicated number of seconds.（RFC 5861 §3）</p></blockquote>
<p><code>stale-while-revalidate</code> 的使用條件：</p>
<ul>
<li><strong>背景更新要靠請求觸發</strong>。只有在窗口內有請求進來，快取才會開始背景驗證；窗口內完全沒有請求，窗口結束後副本就照一般規則處理（RFC 5861 §3.1）。規範建議以請求觸發背景驗證，理由是避免放大攻擊（amplification attack，RFC 5861 §5）。</li>
<li><strong>使用者可能拿到的最舊內容，年齡是 <code>max-age</code> 加上 <code>stale-while-revalidate</code> 的值</strong>。以 <code>max-age=600, stale-while-revalidate=600</code> 為例，使用者最長可能拿到 1200 秒前產生的內容（RFC 5861 §3.1 的例子）。</li>
<li><strong>窗口過後還沒更新成功，就不應該（SHOULD NOT）再給出過期副本</strong>，除非符合規範允許的另一種例外，例如連不上 origin（RFC 5861 §3）。</li>
</ul>
<p>站內 <a href="/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">Stale-While-Revalidate</a> 卡從使用者體驗與新鮮度的取捨描述同一個機制。</p>
<h2 id="stale-if-error">stale-if-error</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Cache-Control: max-age=600, stale-if-error=1200</span></span></code></pre></div><p>這組標頭的意思是：過期之後，如果向 origin 驗證時遇到錯誤，快取可以在 1200 秒內改給過期副本，不把錯誤轉給客戶端（RFC 5861 §4）；這 1200 秒是 <code>stale-if-error</code> 的窗口。規範對「錯誤」的定義是會產生 500、502、503、504 的情形，包括連不上 origin。</p>
<blockquote>
<p>when an error is encountered, a cached stale response MAY be used to satisfy the request, regardless of other freshness information.（RFC 5861 §4）</p></blockquote>
<p>它也可以出現在請求裡，作用範圍只限那一個請求。它的用途是把 origin 的短暫故障擋在快取這一層，站內 <a href="/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">Stale-If-Error</a> 卡把它放在 origin 保護的脈絡下討論。</p>
<h2 id="stale-while-revalidate-與-stale-if-error-的實作支援差異">stale-while-revalidate 與 stale-if-error 的實作支援差異</h2>
<p>這兩個指令的實作支援差異很大。實測 Varnish 7.7 與 nginx 1.27（預設設定，origin 回 <code>max-age=5</code> 加上其中一個指令；最後一列不加 stale 指令，改用 <code>max-age=10</code>）：</p>
<table>
  <thead>
      <tr>
          <th>情形</th>
          <th>Varnish 7.7</th>
          <th>nginx 1.27 proxy_cache</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>stale-while-revalidate=30</code>，origin 正常</td>
          <td>過期後先給過期副本，同時在背景抓新的</td>
          <td>過期後同步回 origin 抓新的，請求要等；預設沒有打開 <code>proxy_cache_background_update</code></td>
      </tr>
      <tr>
          <td><code>stale-while-revalidate=30</code>，origin 停機</td>
          <td>窗口內給過期副本，超過後回 503</td>
          <td>窗口內給過期副本，超過後回 504</td>
      </tr>
      <tr>
          <td><code>stale-if-error=60</code>，origin 停機</td>
          <td>不認得這個指令，只靠自己的預設寬限時間延用過期副本，之後回 503</td>
          <td>窗口內給過期副本</td>
      </tr>
      <tr>
          <td>沒有任何 stale 指令，<code>max-age=10</code></td>
          <td>過期後仍有一段寬限時間（參數 <code>default_grace</code>）先給過期副本、背景更新</td>
          <td>過期後同步回 origin</td>
      </tr>
  </tbody>
</table>
<p>沒有任何 stale 指令時，Varnish 預設也會在過期後的一段時間內給出過期副本。規範的預設是不給出過期副本，所以這是比規範寬鬆的實作預設：沒寫任何 stale 指令的回應（例如商品價格）放在 Varnish 後面，過期後仍可能被給出一小段時間。</p>
<p>在瀏覽器上，WHATWG Fetch 規範的 <code>default</code> 快取模式（見 <a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a> 的〈Fetch 規範的快取模式〉）納入了 <code>stale-while-revalidate</code>：快取裡的副本已過期、但還在窗口內時，先回傳它，同時發出條件請求更新快取。<code>stale-if-error</code> 在瀏覽器的支援情形查各自的文件。CDN 對這兩個指令的支援與上限，同樣查各家文件。</p>
<h2 id="stale-while-revalidate-與-stale-if-error-的選擇">stale-while-revalidate 與 stale-if-error 的選擇</h2>
<p>兩個指令回答的是不同的問題：</p>
<ul>
<li><strong><code>stale-while-revalidate</code></strong> 回答「內容過期一小段時間，使用者拿到舊版可以接受嗎」。可以接受的內容（文章列表、商品介紹）用它換取每次都不必等 origin 回應；不能接受的內容（庫存數字、權限相關的回應）不用它。</li>
<li><strong><code>stale-if-error</code></strong> 回答「origin 故障時，給舊內容比給錯誤頁好嗎」。對多數讀取型內容答案是肯定的；對寫入結果、付款狀態這類「舊答案會造成錯誤行為」的內容，答案是否定的，那類回應用 <code>no-cache</code>，每次使用前都向 origin 確認；<code>must-revalidate</code> 只適合新鮮期間內可以接受舊值、過期後不能延用的內容（兩者的差別見 <a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a>）。<code>must-revalidate</code> 禁止給出過期副本是規範層的要求；Varnish 7.7 的原始碼裡，寬限時間預設取 <code>default_grace</code>，不因 <code>must-revalidate</code> 關閉，所以預設設定下帶 <code>must-revalidate</code> 的副本仍會在寬限時間內被給出，要在 VCL 裡另外處理（<code>no-cache</code> 的回應 Varnish 預設不保存，沒有這個問題）。</li>
</ul>
<p>兩個窗口都會延長內容改版後全面生效的時間：正常運作時延長的是 <code>stale-while-revalidate</code> 的窗口，origin 出錯期間延長的是 <code>stale-if-error</code> 的窗口，加在 <a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 的〈等待到期與改版全面生效的時間〉列出的其他成分之上。決定窗口長度時，要同時看 <a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 裡各種更新手段的作用範圍：瀏覽器、CDN、反向代理這幾層快取當中，哪一層的副本清得掉、哪一層只能等到期。</p>
]]></content:encoded></item><item><title>12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面</title><link>https://tarrragon.github.io/blog/backend/12-http-caching/applying-cache-headers/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/12-http-caching/applying-cache-headers/</guid><description>&lt;p>這篇把本模組前面各章的規則用在幾種常見的回應上，逐一說明標頭怎麼選、選擇的依據落在哪一章的哪條規則。每一種回應都從同一組問題出發：這份回應能不能留在使用者的裝置上（&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store&lt;/a>）、可以給哪些人（&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization&lt;/a>）、內容會不會改（&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限&lt;/a>）、過期或 origin 出錯時能不能給舊的（&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error&lt;/a>）、改了之後最慢多久要讓所有人看到（&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期&lt;/a>）。&lt;/p>
&lt;h2 id="轉址回應的狀態碼與快取標頭">轉址回應的狀態碼與快取標頭&lt;/h2>
&lt;p>轉址回應有兩個獨立的設定：&lt;strong>狀態碼&lt;/strong>決定轉址的語意，&lt;strong>快取標頭&lt;/strong>決定各處快取把這個轉址當作新鮮多久。&lt;/p>
&lt;p>狀態碼的語意由 RFC 9110 定義：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態碼&lt;/th>
 &lt;th>語意&lt;/th>
 &lt;th>重送時方法是否保留&lt;/th>
 &lt;th>沒有快取標頭時能否被啟發式保存&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>301&lt;/td>
 &lt;td>永久移動&lt;/td>
 &lt;td>歷史上可能把 POST 改成 GET&lt;/td>
 &lt;td>能&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>308&lt;/td>
 &lt;td>永久移動&lt;/td>
 &lt;td>保留&lt;/td>
 &lt;td>能&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>302&lt;/td>
 &lt;td>暫時移動&lt;/td>
 &lt;td>歷史上可能把 POST 改成 GET&lt;/td>
 &lt;td>規範不允許&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>307&lt;/td>
 &lt;td>暫時移動&lt;/td>
 &lt;td>保留&lt;/td>
 &lt;td>規範不允許&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「永久」與「暫時」的差別在於轉址的對象會不會再變：RFC 9110 對永久轉址的說明是，能編輯連結的客戶端應該把指向原網址的引用改成新網址（RFC 9110 §15.4.2）。快取標頭則決定各處快取直接使用這個轉址多久；回應沒有快取標頭時，301 與 308 落入 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限&lt;/a> 的啟發式期限，在 Chromium 上永不過期。實測時 Chrome 保存了一個沒有快取標頭的 301，之後 origin 改了目的地，Chrome 仍然直接去舊的目的地，沒有向 origin 送出請求；同一個 301 加上 &lt;code>Cache-Control: max-age=5&lt;/code> 之後，過了 5 秒 Chrome 就回到 origin 取得新的目的地。&lt;/p>
&lt;p>轉址回應的快取標頭由兩件事決定：共用快取可不可以保存（加不加 &lt;code>private&lt;/code>），以及轉址設錯或要改時，願意等多久讓已經存下它的瀏覽器改過來（&lt;code>max-age&lt;/code> 的長度）。轉址常見的三種情形如下，前兩種依目的地會不會變區分：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>目的地確定不會再變&lt;/strong>（例如 HTTP 轉 HTTPS、網域搬遷後的舊網址；網域送了 HSTS 時，瀏覽器會在本機直接改用 HTTPS、不送出請求，之後要撤回 HTTPS 受的是 HSTS 的期限約束，HSTS 不在本模組範圍）：301 或 308，並明寫一個有限的 &lt;code>max-age&lt;/code>。期限的長度以「這條轉址設錯時，願意花多久等它在所有瀏覽器上過期」來定：origin 沒有辦法讓所有使用者瀏覽器裡的副本一起失效：&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期&lt;/a> 的〈各種更新手段的作用範圍〉裡，作用範圍涵蓋全部使用者瀏覽器的手段只有版本化網址與等待到期，而轉址的網址就是使用者手上的連結、換不掉，剩下的只有等待到期。不寫期限等於把這個時間交給各瀏覽器的啟發式規則。&lt;/li>
&lt;li>&lt;strong>目的地可能會改&lt;/strong>（例如可修改目的地的短網址、活動連結）：用 302 或 307 並明寫快取標頭（例如 &lt;code>Cache-Control: private, max-age=&amp;lt;秒數&amp;gt;&lt;/code> 或 &lt;code>no-store&lt;/code>），或用 301 並附上 &lt;code>Cache-Control: private, max-age=&amp;lt;秒數&amp;gt;&lt;/code>。302 與 307 依規範不會被啟發式保存，但部分 CDN 的預設會替沒有快取標頭的轉址套用保存時間（&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限&lt;/a>），所以不論哪個狀態碼都明寫標頭。&lt;code>private&lt;/code> 讓 CDN 與轉送代理不保存，&lt;code>&amp;lt;秒數&amp;gt;&lt;/code> 填的是 origin 改了目的地之後，最慢多久要讓已經存下這個轉址的瀏覽器改去新的目的地。2026-10-02 對 bit.ly 與 t.co 的短網址各送一次請求，兩者的回應都是 301 加上 &lt;code>private&lt;/code> 與很短的 &lt;code>max-age&lt;/code>。&lt;/li>
&lt;li>&lt;strong>要統計每一次點擊&lt;/strong>：同樣用 302、307，或 301 加 &lt;code>private&lt;/code> 與短 &lt;code>max-age&lt;/code>；&lt;code>max-age&lt;/code> 的長度同時決定同一個瀏覽器多久內的重複點擊不會送到 origin、也就不會被計入點擊數。&lt;/li>
&lt;/ul>
&lt;p>已經發出、沒有快取標頭的 301，在 Chromium 上永不過期，事後在 origin 補上 &lt;code>max-age&lt;/code> 傳不到已經存下它的 Chromium 瀏覽器：它們不會再向 origin 送出這個網址的請求。剩下的處置都繞過這個副本：讓舊的目的地網址本身再轉址到新的目的地（前提是舊目的地還在自己手上），或在使用者之後造訪同一個主機的其他網址時，用 &lt;code>Clear-Site-Data&lt;/code> 清掉，代價是這個主機在那個瀏覽器裡的快取條目會全部清掉（&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期&lt;/a> 的〈Clear-Site-Data〉）。&lt;/p></description><content:encoded><![CDATA[<p>這篇把本模組前面各章的規則用在幾種常見的回應上，逐一說明標頭怎麼選、選擇的依據落在哪一章的哪條規則。每一種回應都從同一組問題出發：這份回應能不能留在使用者的裝置上（<a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a>）、可以給哪些人（<a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a>）、內容會不會改（<a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>）、過期或 origin 出錯時能不能給舊的（<a href="/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</a>）、改了之後最慢多久要讓所有人看到（<a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a>）。</p>
<h2 id="轉址回應的狀態碼與快取標頭">轉址回應的狀態碼與快取標頭</h2>
<p>轉址回應有兩個獨立的設定：<strong>狀態碼</strong>決定轉址的語意，<strong>快取標頭</strong>決定各處快取把這個轉址當作新鮮多久。</p>
<p>狀態碼的語意由 RFC 9110 定義：</p>
<table>
  <thead>
      <tr>
          <th>狀態碼</th>
          <th>語意</th>
          <th>重送時方法是否保留</th>
          <th>沒有快取標頭時能否被啟發式保存</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>301</td>
          <td>永久移動</td>
          <td>歷史上可能把 POST 改成 GET</td>
          <td>能</td>
      </tr>
      <tr>
          <td>308</td>
          <td>永久移動</td>
          <td>保留</td>
          <td>能</td>
      </tr>
      <tr>
          <td>302</td>
          <td>暫時移動</td>
          <td>歷史上可能把 POST 改成 GET</td>
          <td>規範不允許</td>
      </tr>
      <tr>
          <td>307</td>
          <td>暫時移動</td>
          <td>保留</td>
          <td>規範不允許</td>
      </tr>
  </tbody>
</table>
<p>「永久」與「暫時」的差別在於轉址的對象會不會再變：RFC 9110 對永久轉址的說明是，能編輯連結的客戶端應該把指向原網址的引用改成新網址（RFC 9110 §15.4.2）。快取標頭則決定各處快取直接使用這個轉址多久；回應沒有快取標頭時，301 與 308 落入 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a> 的啟發式期限，在 Chromium 上永不過期。實測時 Chrome 保存了一個沒有快取標頭的 301，之後 origin 改了目的地，Chrome 仍然直接去舊的目的地，沒有向 origin 送出請求；同一個 301 加上 <code>Cache-Control: max-age=5</code> 之後，過了 5 秒 Chrome 就回到 origin 取得新的目的地。</p>
<p>轉址回應的快取標頭由兩件事決定：共用快取可不可以保存（加不加 <code>private</code>），以及轉址設錯或要改時，願意等多久讓已經存下它的瀏覽器改過來（<code>max-age</code> 的長度）。轉址常見的三種情形如下，前兩種依目的地會不會變區分：</p>
<ul>
<li><strong>目的地確定不會再變</strong>（例如 HTTP 轉 HTTPS、網域搬遷後的舊網址；網域送了 HSTS 時，瀏覽器會在本機直接改用 HTTPS、不送出請求，之後要撤回 HTTPS 受的是 HSTS 的期限約束，HSTS 不在本模組範圍）：301 或 308，並明寫一個有限的 <code>max-age</code>。期限的長度以「這條轉址設錯時，願意花多久等它在所有瀏覽器上過期」來定：origin 沒有辦法讓所有使用者瀏覽器裡的副本一起失效：<a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 的〈各種更新手段的作用範圍〉裡，作用範圍涵蓋全部使用者瀏覽器的手段只有版本化網址與等待到期，而轉址的網址就是使用者手上的連結、換不掉，剩下的只有等待到期。不寫期限等於把這個時間交給各瀏覽器的啟發式規則。</li>
<li><strong>目的地可能會改</strong>（例如可修改目的地的短網址、活動連結）：用 302 或 307 並明寫快取標頭（例如 <code>Cache-Control: private, max-age=&lt;秒數&gt;</code> 或 <code>no-store</code>），或用 301 並附上 <code>Cache-Control: private, max-age=&lt;秒數&gt;</code>。302 與 307 依規範不會被啟發式保存，但部分 CDN 的預設會替沒有快取標頭的轉址套用保存時間（<a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>），所以不論哪個狀態碼都明寫標頭。<code>private</code> 讓 CDN 與轉送代理不保存，<code>&lt;秒數&gt;</code> 填的是 origin 改了目的地之後，最慢多久要讓已經存下這個轉址的瀏覽器改去新的目的地。2026-10-02 對 bit.ly 與 t.co 的短網址各送一次請求，兩者的回應都是 301 加上 <code>private</code> 與很短的 <code>max-age</code>。</li>
<li><strong>要統計每一次點擊</strong>：同樣用 302、307，或 301 加 <code>private</code> 與短 <code>max-age</code>；<code>max-age</code> 的長度同時決定同一個瀏覽器多久內的重複點擊不會送到 origin、也就不會被計入點擊數。</li>
</ul>
<p>已經發出、沒有快取標頭的 301，在 Chromium 上永不過期，事後在 origin 補上 <code>max-age</code> 傳不到已經存下它的 Chromium 瀏覽器：它們不會再向 origin 送出這個網址的請求。剩下的處置都繞過這個副本：讓舊的目的地網址本身再轉址到新的目的地（前提是舊目的地還在自己手上），或在使用者之後造訪同一個主機的其他網址時，用 <code>Clear-Site-Data</code> 清掉，代價是這個主機在那個瀏覽器裡的快取條目會全部清掉（<a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 的〈Clear-Site-Data〉）。</p>
<p>短網址服務的轉址也照這組規則選標頭；短網址特有的取捨（撤回延遲、點擊計數、濫用處理，以及短網址服務保留 301 的理由）見 <a href="/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作</a>。</p>
<h2 id="需登入的-api-回應的快取標頭">需登入的 API 回應的快取標頭</h2>
<p>需要登入才能取得的 API 回應，內容只屬於發出請求的使用者。規範對帶 <code>Authorization</code> 的請求有保護規則：共用快取不能把回應拿去回應別人，除非回應明確允許（RFC 9111 §3.5）。這條規則在 <a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a> 的實測裡，被預設設定的 nginx 1.27 proxy_cache 違反了：使用者甲的回應被發給了使用者乙與沒有登入的請求。而以 Cookie 識別使用者的服務，規範本來就沒有對應的保護規則。</p>
<p>所以需登入的 API 回應，由 origin 明寫快取標頭，不依賴快取對請求的判斷：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Cache-Control: private, no-cache
</span></span><span class="line"><span class="ln">2</span><span class="cl">ETag: &#34;&lt;驗證子&gt;&#34;</span></span></code></pre></div><ul>
<li><code>private</code>：共用快取不保存，使用者自己的瀏覽器可以保存。前提是回應沒有另外帶 <code>CDN-Cache-Control</code> 或 <code>Surrogate-Control</code>，或這些欄位裡也寫了 <code>private</code>、<code>no-store</code>（Varnish 的 <code>Surrogate-Control</code> 只認 <code>no-store</code>）；實作那些欄位的共用快取會忽略 <code>Cache-Control</code> 裡的 <code>private</code>（<a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a> 的〈把指令指定給特定快取：CDN-Cache-Control〉）。</li>
<li><code>no-cache</code>：瀏覽器每次使用前都送條件請求確認；內容沒變時 origin 回 304，省下傳輸內容的成本（<a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a>）。</li>
<li><code>ETag</code>：給條件請求比對用的驗證子，<code>&lt;驗證子&gt;</code> 換成這份內容的版本識別字串，例如內容的雜湊值或資料的更新版本號。</li>
</ul>
<p>內容不能留在任何快取裡、包括使用者自己的裝置（共用電腦上的醫療或金融資料），改用 <code>Cache-Control: no-store</code>，代價是每次都要完整重抓。</p>
<h2 id="個人化頁面的快取標頭">個人化頁面的快取標頭</h2>
<p>個人化的 HTML 頁面（帶使用者名稱的首頁、購物車頁面）和需登入的 API 回應用同一組標頭：<code>private</code> 加 <code>no-cache</code> 加 <code>ETag</code>。兩個額外的風險：</p>
<ul>
<li><strong>回應帶 <code>Set-Cookie</code></strong>：規範不禁止保存帶 <code>Set-Cookie</code> 的回應（RFC 9111 §7.3）。一個沒有 <code>private</code> 的頁面同時設定了 session Cookie，規範允許共用快取把它連同 Cookie 一起發給下一個使用者。<code>private</code> 同時擋住了這個風險。</li>
<li><strong>上一頁還原</strong>：瀏覽器的 back/forward cache 不受 <code>Cache-Control</code> 約束，登出之後按上一頁仍可能看到登出前的畫面（<a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a>）。這要由網頁程式在 <code>pageshow</code> 事件裡檢查 <code>event.persisted</code>（為 true 代表頁面是從 back/forward cache 還原的），再重新確認登入狀態，快取標頭處理不了。</li>
</ul>
<h2 id="靜態資源與引用它的頁面的快取標頭">靜態資源與引用它的頁面的快取標頭</h2>
<p>JS、CSS、圖片這類靜態資源，和引用它們的 HTML 頁面要分開設定（<a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 的版本化網址）：</p>
<table>
  <thead>
      <tr>
          <th>回應</th>
          <th>標頭</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>版本化網址的靜態資源（<code>/static/app.3f9a1c.js</code>）</td>
          <td><code>Cache-Control: public, max-age=31536000, immutable</code></td>
          <td>內容改了就換網址，同一個網址的內容永遠不變，每一層快取都可以在一年內直接使用它</td>
      </tr>
      <tr>
          <td>引用它們的 HTML 頁面</td>
          <td><code>Cache-Control: no-cache</code> 加 <code>ETag</code></td>
          <td>頁面是改版的入口，每次都要確認，使用者才會拿到引用新資源網址的版本</td>
      </tr>
  </tbody>
</table>
<p>靜態資源那一列的 <code>public</code> 在多數情形是多餘的（<a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a>），寫上它是為了讓意圖明確。<code>31536000</code> 是一年的秒數，是常見的取值，不是規範規定的上限。</p>
<p>HTML 頁面那一列放在共用快取後面時有一個代價：實測的 Varnish 與 nginx 預設不保存 <code>no-cache</code> 的回應，每個請求都會到 origin（<a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a> 的〈共用快取實作的驗證預設〉）。要讓 CDN 也保存 HTML，用只給 CDN 的期限，例如 <code>CDN-Cache-Control: max-age=600</code>，<code>Cache-Control</code> 維持 <code>no-cache</code>：實作 RFC 9213 的 CDN 依它保存並忽略 <code>Cache-Control</code>，其他快取照 <code>no-cache</code>（<a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a> 的〈把指令指定給特定快取：CDN-Cache-Control〉）。<code>s-maxage</code> 不適合這個用途：它同時作用於清不掉的轉送代理，而且和 <code>no-cache</code> 並存時，共用快取每次使用前仍要驗證。改版時再用 purge 清掉 CDN 上的副本（<a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 的〈purge〉）。這組設定還有一個前提：網站沒有用 Service Worker 以快取優先的策略攔截頁面請求，否則 HTML 的 <code>no-cache</code> 不會生效。</p>
<h2 id="公開且會變動的回應的快取標頭">公開且會變動的回應的快取標頭</h2>
<p>不分使用者、但內容會變動的回應（文章列表、商品資訊、公開的讀取 API），可以讓每一層快取保存，期限由業務能容忍多久的舊資料決定，例如 <code>Cache-Control: max-age=&lt;秒數&gt;, stale-while-revalidate=&lt;秒數&gt;, stale-if-error=&lt;秒數&gt;</code>：<code>max-age</code> 是可以直接使用的時間，兩個 stale 窗口讓過期之後與 origin 出錯時仍有內容可給（<a href="/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</a>）。舊資料會造成錯誤行為的回應（付款狀態、庫存數字）不加 stale 指令，改用 <code>no-cache</code>。</p>
<p>錯誤回應也要明寫標頭：404 屬於可啟發式快取的狀態碼，資源上線前被查過一次，沒有快取標頭的 404 就可能留在快取裡，上線之後仍回 404。實測的 Varnish 會把它保存 <code>default_ttl</code> 的時間，Chromium 與 Firefox 的啟發式規則對 404 不保存，CDN 查各家的預設。</p>
<h2 id="檢查實際送出的快取標頭">檢查實際送出的快取標頭</h2>
<p>設定寫在 origin 的程式或伺服器設定裡，但使用者實際拿到的標頭，可能被 CDN、反向代理或框架改過。檢查方式是從外部送請求，看回應：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 只看標頭：-s 不顯示進度，-I 送 HEAD 請求並印出回應標頭</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">curl -sI https://&lt;網域&gt;/&lt;路徑&gt;
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 送 GET、只印標頭：-D - 把回應標頭印到標準輸出，-o /dev/null 丟掉內容</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">curl -s -D - -o /dev/null https://&lt;網域&gt;/&lt;路徑&gt;
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 帶身分送 GET，標頭與內容都印出來，再拿去和不帶身分的結果比對</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">curl -s -D - -H <span class="s2">&#34;Authorization: Bearer &lt;token&gt;&#34;</span> https://&lt;網域&gt;/&lt;路徑&gt;
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 以 Cookie 識別身分的服務，改帶 Cookie</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">curl -s -D - -H <span class="s2">&#34;Cookie: &lt;cookie&gt;&#34;</span> https://&lt;網域&gt;/&lt;路徑&gt;
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 繞過 CDN、直接連 origin，取得 origin 原本送出的標頭，再和上面經過 CDN 的結果比對</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">curl -sI --resolve &lt;網域&gt;:443:&lt;origin 的 IP&gt; https://&lt;網域&gt;/&lt;路徑&gt;</span></span></code></pre></div><p><code>&lt;網域&gt;/&lt;路徑&gt;</code> 換成要檢查的網址；<code>&lt;token&gt;</code> 換成某個測試帳號登入後拿到的存取權杖，<code>Bearer</code> 是權杖類型，服務用的若是別種驗證方式就換成它要的格式；<code>&lt;cookie&gt;</code> 填那個測試帳號登入後的 Cookie 值；<code>&lt;origin 的 IP&gt;</code> 填 origin 伺服器自己的位址，也就是 CDN 設定裡的回源位址，<code>--resolve</code> 讓 curl 把這個網域直接連到那個位址、不經過 CDN。規範要求伺服器對 HEAD 回應的標頭應該（SHOULD）與 GET 相同，但允許省略只有產生內容時才決定的欄位，例如 <code>Vary</code>（RFC 9110 §9.3.2），所以檢查 <code>Vary</code> 時用送 GET、只印標頭的那條指令。</p>
<p>看回應時注意四件事：</p>
<ul>
<li><code>Cache-Control</code> 是不是 origin 設定的那一組，有沒有被中間的 CDN、反向代理或框架改寫或附加。</li>
<li>有沒有 <code>Age</code>：有就代表這份回應來自某個快取，值是副本的年齡（<a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>）；沒有不代表一定來自 origin（實測的 nginx 1.27 proxy_cache 不送 <code>Age</code>）。</li>
<li>CDN 自己加的命中標頭（名稱各家不同，查各家文件）：看這次是命中快取還是回到 origin。</li>
<li>先跑帶身分的那條，讓沿路的快取有機會存下這份回應；在共用快取保存這份回應的期限內（看 <code>s-maxage</code>、<code>max-age</code>、<code>Expires</code>、<code>CDN-Cache-Control</code> 或 CDN 設定的保存時間，都沒有就緊接著跑），再跑拿掉 <code>-H</code> 的同一條，兩次都連對外的網址（經過 CDN 與反向代理），比對兩份內容。不帶身分卻拿到測試帳號的內容，就是 <a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a> 描述的外洩。兩份內容沒有差異，不能證明沒有外洩：兩次請求可能落在 CDN 的不同節點，要多跑幾次並對照命中標頭。順序顛倒時檢查的是另一件事：快取存下的匿名回應，會不會被發給登入的使用者。</li>
</ul>
]]></content:encoded></item><item><title>模組十二：HTTP 快取與回應保存規則</title><link>https://tarrragon.github.io/blog/backend/12-http-caching/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/12-http-caching/</guid><description>&lt;p>HTTP 快取模組處理的是應用層快取以外的那一段：一個 HTTP 回應離開 origin 之後，瀏覽器、CDN、反向代理會另外保存它的副本，之後的請求由這些副本回應，不再送到 origin。應用層快取（Redis 這類由服務自己讀寫的快取）在 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">模組二：快取與 Redis&lt;/a> 處理；本模組處理的副本存放在 origin 不經營、也不直接讀寫的位置。&lt;/p>
&lt;h2 id="推導源頭">推導源頭&lt;/h2>
&lt;p>本模組各章從同一個事實展開：&lt;strong>回應離開 origin 之後，origin 能交給下游副本的指令只有回應當下附帶的標頭，而每個快取依自己的實作預設解讀這些指令。&lt;/strong> 由這個事實依序推出：&lt;/p>
&lt;ul>
&lt;li>副本存放在哪些快取、各由誰經營（12.1）&lt;/li>
&lt;li>副本在不詢問 origin 的情況下可以直接使用多久（12.2）&lt;/li>
&lt;li>副本過期之後怎麼向 origin 確認是否仍然有效（12.3）&lt;/li>
&lt;li>一個請求會拿到哪一份副本（12.4）&lt;/li>
&lt;li>origin 想改變已存副本時，各種更新手段的作用範圍（12.5）&lt;/li>
&lt;li>在什麼條件下允許給出過期副本（12.6）&lt;/li>
&lt;li>瀏覽器在重新整理與上一頁時怎麼使用它的副本（12.7）&lt;/li>
&lt;li>把前述規則用在具體回應上（12.8）&lt;/li>
&lt;/ul>
&lt;h2 id="讀者定位">讀者定位&lt;/h2>
&lt;p>讀者是寫過 HTTP 服務、知道狀態碼與標頭是什麼，但沒有系統讀過 HTTP 快取規範的後端工程師。他缺的是兩件事：標頭在各種快取上的精確語意，以及規範與實作之間的落差。&lt;/p>
&lt;h2 id="規範與實作預設的分界">規範與實作預設的分界&lt;/h2>
&lt;p>本模組每一章都區分兩類行為：&lt;strong>規範規定的行為&lt;/strong>（RFC 9111 HTTP Caching、RFC 9110 HTTP Semantics 與相關擴充），以及&lt;strong>某個快取實作的預設&lt;/strong>。實測一次 Varnish 7.7、nginx 1.27 與 Chrome 154 的結果顯示，兩者落差很大：同一組標頭，實作的預設可能比規範保守、比規範寬鬆或違反規範，三類的界定寫在 12.1〈可以被保存的回應〉。文章寫出落差的形狀與該去哪個設定項查；預設數值只在說明落差形狀時引用，並標明讀取日期與版本；CDN 的預設行為各家不同，以各家文件為準。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取&lt;/a>&lt;/td>
 &lt;td>私有快取與共用快取的定義、經營者、各自適用的指令&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限&lt;/a>&lt;/td>
 &lt;td>副本可直接使用多久、Age 標頭、沒有指令時的啟發式期限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store&lt;/a>&lt;/td>
 &lt;td>驗證子、條件請求、no-cache 與 no-store 的差別、must-revalidate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization&lt;/a>&lt;/td>
 &lt;td>哪個請求拿到哪份副本、個人化回應在共用快取的處理規則、快取投毒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期&lt;/a>&lt;/td>
 &lt;td>各種更新手段的作用範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error&lt;/a>&lt;/td>
 &lt;td>允許給出過期副本的條件與實作支援差異&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁&lt;/a>&lt;/td>
 &lt;td>一般瀏覽、重新整理與強制重新整理送出的請求、back/forward cache&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面&lt;/a>&lt;/td>
 &lt;td>具體回應的快取標頭選擇&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="和其他模組的分工">和其他模組的分工&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源&lt;/a> 處理 CDN 的營運面（origin 保護、tag purge 的操作模型）；本模組處理標頭語意，5.9 快取分層表的瀏覽器與邊緣層兩列以本模組為準。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">Stale-While-Revalidate&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">Stale-If-Error&lt;/a> 兩張卡直接沿用；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/freshness-window/" data-link-title="Freshness Window" data-link-desc="每類資料能容忍多久的不新鮮？依欄位風險分級定義過時窗口、取代單一全域 TTL">Freshness Window&lt;/a> 卡處理業務能容忍多久的舊資料，12.5 把它和新鮮度期限接上。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作&lt;/a> 的快取標頭一節是 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面&lt;/a> 〈轉址回應的狀態碼與快取標頭〉一節的一個實例。&lt;/li>
&lt;/ul>
&lt;h2 id="研究材料">研究材料&lt;/h2>
&lt;p>規範條文取自 RFC 9111、RFC 9110、RFC 9213、RFC 5861、RFC 8246 原文（2026-10-02 取得）；瀏覽器實作對照 Chromium &lt;code>net/http/http_response_headers.cc&lt;/code> 與 Firefox &lt;code>nsHttpResponseHead.cpp&lt;/code>（同日 main 分支）；實測環境是 docker 上的 Python origin、Varnish 7.7.3、nginx 1.27.5 與 Chrome 154。實測腳本與設定不放進文章，文章只引用觀察結果。&lt;/p></description><content:encoded><![CDATA[<p>HTTP 快取模組處理的是應用層快取以外的那一段：一個 HTTP 回應離開 origin 之後，瀏覽器、CDN、反向代理會另外保存它的副本，之後的請求由這些副本回應，不再送到 origin。應用層快取（Redis 這類由服務自己讀寫的快取）在 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">模組二：快取與 Redis</a> 處理；本模組處理的副本存放在 origin 不經營、也不直接讀寫的位置。</p>
<h2 id="推導源頭">推導源頭</h2>
<p>本模組各章從同一個事實展開：<strong>回應離開 origin 之後，origin 能交給下游副本的指令只有回應當下附帶的標頭，而每個快取依自己的實作預設解讀這些指令。</strong> 由這個事實依序推出：</p>
<ul>
<li>副本存放在哪些快取、各由誰經營（12.1）</li>
<li>副本在不詢問 origin 的情況下可以直接使用多久（12.2）</li>
<li>副本過期之後怎麼向 origin 確認是否仍然有效（12.3）</li>
<li>一個請求會拿到哪一份副本（12.4）</li>
<li>origin 想改變已存副本時，各種更新手段的作用範圍（12.5）</li>
<li>在什麼條件下允許給出過期副本（12.6）</li>
<li>瀏覽器在重新整理與上一頁時怎麼使用它的副本（12.7）</li>
<li>把前述規則用在具體回應上（12.8）</li>
</ul>
<h2 id="讀者定位">讀者定位</h2>
<p>讀者是寫過 HTTP 服務、知道狀態碼與標頭是什麼，但沒有系統讀過 HTTP 快取規範的後端工程師。他缺的是兩件事：標頭在各種快取上的精確語意，以及規範與實作之間的落差。</p>
<h2 id="規範與實作預設的分界">規範與實作預設的分界</h2>
<p>本模組每一章都區分兩類行為：<strong>規範規定的行為</strong>（RFC 9111 HTTP Caching、RFC 9110 HTTP Semantics 與相關擴充），以及<strong>某個快取實作的預設</strong>。實測一次 Varnish 7.7、nginx 1.27 與 Chrome 154 的結果顯示，兩者落差很大：同一組標頭，實作的預設可能比規範保守、比規範寬鬆或違反規範，三類的界定寫在 12.1〈可以被保存的回應〉。文章寫出落差的形狀與該去哪個設定項查；預設數值只在說明落差形狀時引用，並標明讀取日期與版本；CDN 的預設行為各家不同，以各家文件為準。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a></td>
          <td>私有快取與共用快取的定義、經營者、各自適用的指令</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a></td>
          <td>副本可直接使用多久、Age 標頭、沒有指令時的啟發式期限</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a></td>
          <td>驗證子、條件請求、no-cache 與 no-store 的差別、must-revalidate</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a></td>
          <td>哪個請求拿到哪份副本、個人化回應在共用快取的處理規則、快取投毒</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a></td>
          <td>各種更新手段的作用範圍</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/12-http-caching/serving-stale/" data-link-title="12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error" data-link-desc="快取在什麼條件下可以給出已經過期的副本：規範對過期副本的預設禁止、RFC 5861 的 stale-while-revalidate 與 stale-if-error 兩個擴充指令的語意與窗口，以及 Varnish、nginx 與瀏覽器在支援上的差異">12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</a></td>
          <td>允許給出過期副本的條件與實作支援差異</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a></td>
          <td>一般瀏覽、重新整理與強制重新整理送出的請求、back/forward cache</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面</a></td>
          <td>具體回應的快取標頭選擇</td>
      </tr>
  </tbody>
</table>
<h2 id="和其他模組的分工">和其他模組的分工</h2>
<ul>
<li><a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a> 處理 CDN 的營運面（origin 保護、tag purge 的操作模型）；本模組處理標頭語意，5.9 快取分層表的瀏覽器與邊緣層兩列以本模組為準。</li>
<li><a href="/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">Stale-While-Revalidate</a>、<a href="/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">Stale-If-Error</a> 兩張卡直接沿用；<a href="/blog/backend/knowledge-cards/freshness-window/" data-link-title="Freshness Window" data-link-desc="每類資料能容忍多久的不新鮮？依欄位風險分級定義過時窗口、取代單一全域 TTL">Freshness Window</a> 卡處理業務能容忍多久的舊資料，12.5 把它和新鮮度期限接上。</li>
<li><a href="/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作</a> 的快取標頭一節是 <a href="/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面</a> 〈轉址回應的狀態碼與快取標頭〉一節的一個實例。</li>
</ul>
<h2 id="研究材料">研究材料</h2>
<p>規範條文取自 RFC 9111、RFC 9110、RFC 9213、RFC 5861、RFC 8246 原文（2026-10-02 取得）；瀏覽器實作對照 Chromium <code>net/http/http_response_headers.cc</code> 與 Firefox <code>nsHttpResponseHead.cpp</code>（同日 main 分支）；實測環境是 docker 上的 Python origin、Varnish 7.7.3、nginx 1.27.5 與 Chrome 154。實測腳本與設定不放進文章，文章只引用觀察結果。</p>
<h2 id="backlog">Backlog</h2>
<p>格式見 <a href="/blog/posts/backlog-format-spec/" data-link-title="教學模組的 Backlog 段格式規範" data-link-desc="要在模組大綱記錄未完成工作、或要跨模組盤點還有什麼沒寫時，用來決定段名、欄位與哪些內容不進 backlog">Backlog 段格式規範</a>。目前沒有待辦項目。</p>
]]></content:encoded></item><item><title>Cache-Control（快取控制標頭）</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/cache-control/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/cache-control/</guid><description>&lt;p>Cache-Control 是 HTTP 回應（與請求）上的標頭，用一組指令告訴沿路的快取三件事：這份回應能不能保存、保存在哪一種快取、保存之後多久之內可以不問 origin 直接使用。它是 origin 對瀏覽器、CDN 與代理下指令的主要管道，可以直接使用的那段時間就是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a>，超過之後是否允許先給舊版，由 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">Stale-While-Revalidate&lt;/a> 這類擴充指令決定。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Cache-Control 位在 HTTP 快取的指令層，常用指令按職責分組：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>保存位置&lt;/strong>：&lt;code>private&lt;/code> 只讓使用者自己的瀏覽器保存，&lt;code>public&lt;/code> 允許共用快取保存帶 &lt;code>Authorization&lt;/code> 請求的回應，&lt;code>no-store&lt;/code> 讓任何快取都不保存。&lt;/li>
&lt;li>&lt;strong>新鮮度期限&lt;/strong>：&lt;code>max-age&lt;/code> 給所有快取，&lt;code>s-maxage&lt;/code> 只給共用快取並取代 &lt;code>max-age&lt;/code>。兩者都沒有、也沒有 &lt;code>Expires&lt;/code> 時，快取改用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/heuristic-freshness/" data-link-title="Heuristic Freshness（啟發式新鮮度期限）" data-link-desc="回應沒寫任何快取期限卻仍被瀏覽器或 CDN 保存、改了內容使用者卻還拿到舊版時，查快取自己估期限的規則">Heuristic Freshness&lt;/a> 自己估。&lt;/li>
&lt;li>&lt;strong>過期後的處理&lt;/strong>：&lt;code>no-cache&lt;/code> 要求每次使用前都先向 origin 驗證，&lt;code>must-revalidate&lt;/code> 禁止過期之後不經驗證就給出舊版，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">Stale-If-Error&lt;/a> 允許 origin 出錯時給出舊版。&lt;/li>
&lt;/ul>
&lt;p>哪些請求共用同一份保存的回應不由這個標頭管，那是快取鍵與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/vary/" data-link-title="Vary（快取鍵變體標頭）" data-link-desc="同一個網址會依請求標頭回不同內容、或快取命中率異常低時，查這個回應標頭怎麼把請求標頭加進快取鍵">Vary&lt;/a> 的職責。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>一個需要登入才拿得到的 API 回應寫 &lt;code>Cache-Control: private, no-cache&lt;/code> 加上 &lt;code>ETag&lt;/code>：CDN 不保存，使用者的瀏覽器可以保存，但每次使用前都送條件請求確認，內容沒變時 origin 回 304、省下傳輸內容。同一個網站的版本化靜態檔（檔名含內容雜湊）寫 &lt;code>public, max-age=31536000, immutable&lt;/code>，每一層快取都可以在一年內直接使用。&lt;/p>
&lt;p>回應完全沒有 Cache-Control、也沒有 &lt;code>Expires&lt;/code> 時，它仍可能被保存：狀態碼屬於可啟發式快取的種類（例如 200、301、404）時，各快取依自己的規則估一個期限，同一個 301 在不同瀏覽器與代理上得到的期限可能差到從零到永不過期。&lt;/p>
&lt;h2 id="判讀方式">判讀方式&lt;/h2>
&lt;p>讀一個回應的 Cache-Control 時，依序問三件事：它能不能給別人（看 &lt;code>private&lt;/code>／&lt;code>public&lt;/code>；請求帶 &lt;code>Authorization&lt;/code> 時規範禁止共用，但實作不一定遵守，需登入的回應明寫 &lt;code>private&lt;/code>，見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization&lt;/a>）、內容會不會改（看期限指令，沒有期限就是交給各快取自己估）、origin 改了內容之後最慢多久要讓所有人看到（期限加上 stale 窗口，以及上游沒傳 &lt;code>Age&lt;/code> 時各層期限相加，見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期&lt;/a>）。&lt;/p>
&lt;p>名稱與作用容易對不上的兩個指令：&lt;/p>
&lt;ul>
&lt;li>&lt;code>no-cache&lt;/code>：可以保存，但每次使用前都要驗證；讓快取完全不保存的是 &lt;code>no-store&lt;/code>。&lt;/li>
&lt;li>&lt;code>private&lt;/code>：只規定保存位置（共用快取不存），不保護內容本身。另外，回應另外帶了有效的 &lt;code>CDN-Cache-Control&lt;/code>（或該 CDN 認得的同類欄位）時，實作它的 CDN 會整個忽略這則回應的 Cache-Control，&lt;code>private&lt;/code> 也跟著失效。&lt;/li>
&lt;/ul>
&lt;p>完整的規範與實作差異在 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/" data-link-title="模組十二：HTTP 快取與回應保存規則" data-link-desc="整理 HTTP 回應離開 origin 之後被瀏覽器、CDN 與代理保存的規則：保存位置、新鮮度期限、過期後的驗證、快取鍵、已存副本的更新、過期副本的延用、瀏覽器端行為，以及在轉址、API 與個人化頁面上的應用">模組十二：HTTP 快取與回應保存規則&lt;/a>：保存位置與各指令的對象見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取&lt;/a>，期限的取用順序見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限&lt;/a>，&lt;code>no-cache&lt;/code>、&lt;code>no-store&lt;/code>、&lt;code>must-revalidate&lt;/code> 的差別見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store&lt;/a>，具體回應的標頭選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Cache-Control 是 HTTP 回應（與請求）上的標頭，用一組指令告訴沿路的快取三件事：這份回應能不能保存、保存在哪一種快取、保存之後多久之內可以不問 origin 直接使用。它是 origin 對瀏覽器、CDN 與代理下指令的主要管道，可以直接使用的那段時間就是 <a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a>，超過之後是否允許先給舊版，由 <a href="/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">Stale-While-Revalidate</a> 這類擴充指令決定。</p>
<h2 id="概念位置">概念位置</h2>
<p>Cache-Control 位在 HTTP 快取的指令層，常用指令按職責分組：</p>
<ul>
<li><strong>保存位置</strong>：<code>private</code> 只讓使用者自己的瀏覽器保存，<code>public</code> 允許共用快取保存帶 <code>Authorization</code> 請求的回應，<code>no-store</code> 讓任何快取都不保存。</li>
<li><strong>新鮮度期限</strong>：<code>max-age</code> 給所有快取，<code>s-maxage</code> 只給共用快取並取代 <code>max-age</code>。兩者都沒有、也沒有 <code>Expires</code> 時，快取改用 <a href="/blog/backend/knowledge-cards/heuristic-freshness/" data-link-title="Heuristic Freshness（啟發式新鮮度期限）" data-link-desc="回應沒寫任何快取期限卻仍被瀏覽器或 CDN 保存、改了內容使用者卻還拿到舊版時，查快取自己估期限的規則">Heuristic Freshness</a> 自己估。</li>
<li><strong>過期後的處理</strong>：<code>no-cache</code> 要求每次使用前都先向 origin 驗證，<code>must-revalidate</code> 禁止過期之後不經驗證就給出舊版，<a href="/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">Stale-If-Error</a> 允許 origin 出錯時給出舊版。</li>
</ul>
<p>哪些請求共用同一份保存的回應不由這個標頭管，那是快取鍵與 <a href="/blog/backend/knowledge-cards/vary/" data-link-title="Vary（快取鍵變體標頭）" data-link-desc="同一個網址會依請求標頭回不同內容、或快取命中率異常低時，查這個回應標頭怎麼把請求標頭加進快取鍵">Vary</a> 的職責。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>一個需要登入才拿得到的 API 回應寫 <code>Cache-Control: private, no-cache</code> 加上 <code>ETag</code>：CDN 不保存，使用者的瀏覽器可以保存，但每次使用前都送條件請求確認，內容沒變時 origin 回 304、省下傳輸內容。同一個網站的版本化靜態檔（檔名含內容雜湊）寫 <code>public, max-age=31536000, immutable</code>，每一層快取都可以在一年內直接使用。</p>
<p>回應完全沒有 Cache-Control、也沒有 <code>Expires</code> 時，它仍可能被保存：狀態碼屬於可啟發式快取的種類（例如 200、301、404）時，各快取依自己的規則估一個期限，同一個 301 在不同瀏覽器與代理上得到的期限可能差到從零到永不過期。</p>
<h2 id="判讀方式">判讀方式</h2>
<p>讀一個回應的 Cache-Control 時，依序問三件事：它能不能給別人（看 <code>private</code>／<code>public</code>；請求帶 <code>Authorization</code> 時規範禁止共用，但實作不一定遵守，需登入的回應明寫 <code>private</code>，見 <a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a>）、內容會不會改（看期限指令，沒有期限就是交給各快取自己估）、origin 改了內容之後最慢多久要讓所有人看到（期限加上 stale 窗口，以及上游沒傳 <code>Age</code> 時各層期限相加，見 <a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a>）。</p>
<p>名稱與作用容易對不上的兩個指令：</p>
<ul>
<li><code>no-cache</code>：可以保存，但每次使用前都要驗證；讓快取完全不保存的是 <code>no-store</code>。</li>
<li><code>private</code>：只規定保存位置（共用快取不存），不保護內容本身。另外，回應另外帶了有效的 <code>CDN-Cache-Control</code>（或該 CDN 認得的同類欄位）時，實作它的 CDN 會整個忽略這則回應的 Cache-Control，<code>private</code> 也跟著失效。</li>
</ul>
<p>完整的規範與實作差異在 <a href="/blog/backend/12-http-caching/" data-link-title="模組十二：HTTP 快取與回應保存規則" data-link-desc="整理 HTTP 回應離開 origin 之後被瀏覽器、CDN 與代理保存的規則：保存位置、新鮮度期限、過期後的驗證、快取鍵、已存副本的更新、過期副本的延用、瀏覽器端行為，以及在轉址、API 與個人化頁面上的應用">模組十二：HTTP 快取與回應保存規則</a>：保存位置與各指令的對象見 <a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a>，期限的取用順序見 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>，<code>no-cache</code>、<code>no-store</code>、<code>must-revalidate</code> 的差別見 <a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a>，具體回應的標頭選擇見 <a href="/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面</a>。</p>
]]></content:encoded></item></channel></rss>