談 HTTP 標頭(header) Vary 的作用
Posted on May 23, 2024 in HTTP headers by Amo Chen ‐ 4 min read
HTTP header “Vary” 是 1 個很重要的 Header, 它之所以重要,是因為 Vary 會對內容傳遞網路(CDN), 代理伺服器(Proxy)以及瀏覽器等有作用產生。
p.s. 而且用得不好就可能會產生災難⋯⋯
要理解 Vary Header 必須先知道,對 1 組 URL 發出 HTTP request 時,伺服器可以產生不同內容的回應。
例如 GET /index.html
要求取得資源 index.html
時,伺服器可以選擇回應 HTML 格式(text/html)的內容,也可以選擇回應純文字(text/plain)的內容。
但是伺服器到底要挑那一種內容給使用者才對咧?
伺服器回應內容時,會遵循稱為 content negotiation 的標準,這個標準底下又分為 3 種方式:
- Server-driven Negotiation
- Agent-driven Negotiation
- Transparent Negotiation
大家現在瀏覽網頁其實都是 Server-driven Negotiation 為主,其實是 Server 自動幫你挑內容的意思,但 Server 也不是在毫無資訊的情況下亂挑,而是根據 HTTP request headers 裡面有的資訊,例如 Accept
, Accept-Encoding
, Accept-Language
這些 headers 告訴 Server, 發出 request 的 Client 支援什麼格式、偏好什麼編碼、偏好什麼語言等等, Server 就能夠依照這些資訊產生回應,譬如 Accept: text/plain
就代表 Client 只接受純文字回應,所以 Server 有支援純文字回應的情況下,就會回應純文字給 Client 端,但如果不支援就會回應 406 (Not Acceptable)或 415 (Unsupported Media Type)狀態碼告訴你回應有問題,也有可能直接不管 Client 的偏好直接回應內容。
至於 Agent-driven Negotiation 則是由 Client 端選擇要什麼內容, Server 則會回應有哪些內容可以選擇;而 Transparent Negotiation 則是結合 Server-driven Negotiation 與 Agent-driven Negotiation 兩種機制,簡單來講是有 cache 時就選 Server-driven Negotiation 模式,沒有 cache 就選 Agent-driven Negotiation 模式。因為 Agent-driven Negotiation 與 Transparent Negotiation 目前不是主流,所以有興趣的人請自行研讀 RFC-2616 的細節囉!
知道 content negotiation 之後,再來談談瀏覽器這邊的快取(cache)。
現代瀏覽器支援多種方式的快取,這些快取一方面可以增加瀏覽器渲染的速度,一方面也可以減少發往伺服器端的要求數,伺服器可以告訴瀏覽器將回應的內容快取起來,如果之後請求相同資源,就直接用快取回應,其中最簡單的快取方式就是使用 URL 當作快取的 Key 。
這時候問題來了,既然伺服器針對同 1 個 URL 的請求,可以產生不同內容的回應,那只針對 URL 所做的快取機制不就造成問題?
例如同樣是 GET /index.html
,瀏覽器使用 Accept-Language: en
發出要求,然後 Server 也以 en 進行回應,並且告訴瀏覽器要快取回應內容,接著瀏覽器又改使用 Accept-Language: de
發出要求時,因為都是請求 GET /index.html
, 所以瀏覽器就會直接拿 en 的回應內容給使用者看⋯⋯。
相同的問題也會發生使用代理伺服器(proxy)的情況下,因為代理伺服器也是使用類似的快取方式。
所以 Server 需要 1 個額外的手段告訴瀏覽器、代理伺服器要使用快取時,還需要額外考慮什麼,那就是 Vary header 的作用!
The Vary field value indicates the set of request-header fields that fully determines, while the response is fresh, whether a cache is permitted to use the response to reply to a subsequent request without revalidation.
舉前述 Accept-Language
變動的例子來說,因為 Server 會根據 Client 的 Accept-Language
產生不同的內容,所以 Server 需要在回應時加上 Vary: Accept-Language
HTTP header, 藉此告訴瀏覽器、代理伺服器,這個內容會因為 Accept-Language
值的不同而「可能」變動,所以瀏覽器跟代理伺服器在做 cache key 時,就會一起將 Accept-Language
納入,一旦對相同 URL 的請求的 Accept-Language
值有所不同,它就會選擇不要使用快取!
說那麼多,實驗一次最有感覺!
Vary header 的作用可以在 local 環境使用下列步驟實驗。
使用以下程式碼建立 1 個極簡 HTTP Server, 該 sever 的回應會請 Client 端快取 1 天之外,也使用 Vary header 告訴 Client 須考慮 X-DEVICE
才能決定要不要使用快取的內容:
app.py
import json
import time
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
device = request.headers.get('X-DEVICE', 'UNKNOWN')
return f'<html><body><h1>{device}</h1></body></html>', {
'Content-Type': 'text/html',
'Vary': 'X-DEVICE',
'Cache-Control': 'max-age=86400'
}
if __name__ == '__main__':
app.run(debug=True)
接著用 Python 執行 app.py
:
$ python app.py
打開瀏覽器,輸入網址 http://127.0.0.1/
可以看到以下畫面:
最後打開發者工具,在 Conosole
輸入以下 JavaScript 程式碼 3 次:
await fetch('/', {headers: {'X-DEVICE': 'a', 'X-PLATFORM': 'b'}})
再來切換到 Network
將可以發現有 2 個 requests 是直接從 cache 回應,回應的恰好都是 {'X-DEVICE': 'a', 'X-PLATFORM': 'b'}
的要求:
最後,再將 X-DEVICE
改為 b
再發送一次 request, 又會發現這次不會從 cache 回應了:
之所以會有這些行為差異,就是因為瀏覽器按照 Vary 的規定,將 X-DEVICE
也納入要不要使用 cache 的考量,一旦 X-DEVICE
有變動,就不會使用既有的 cache 。
p.s. 如果有興趣的話,可以改看看 X-PLATFORM
,不管怎麼改都會直接從 cache 回應。
這就是 Vary header 的作用!
最後講講 Vary header 的規定, Vary header 只能放 request 的 headers, 因為 Server 是根據 request 的 headers 決定內容,如果有多個 request headers 會影響 Server 產生內容的話, Server 端可以用逗號分隔指定,例如:
Vary: Accept, Accept-Language
但如果看到 Vary: *
則代表資源 uncacheable, 不是把所有 headers 都考慮進去的意思喔!
使用上,如果 1 項資源屬於可快取的話,「應該」要附上 Vary header 在 Server 的 response 。
另外, Vary: Authorization
是不需要的,因為 Authorization header 本身就規定禁止為不同使用者回應相同 cache 。
References
https://datatracker.ietf.org/doc/html/rfc2616#section-12
https://httpwg.org/specs/rfc7234.html#caching.negotiated.responses