<?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>Etag on Tarragon</title><link>https://tarrragon.github.io/blog/tags/etag/</link><description>Recent content in Etag 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/etag/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>