ETagとは

# Document
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/ETag

https://datatracker.ietf.org/doc/html/rfc7232#section-2.3

# 概要
ブラウザキャッシュを利用する仕組み。
それを実現するためのHTTPレスポンスHeader。

ブラウザにて、コンテンツに対するHashを持ち、これを最新のコンテンツ要求時に添える。
サーバ側は送られたHashと最新のコンテンツのHashを比較し、更新の有無をクライアントへ返却する。
更新が無ければコンテンツは返送しない。

## 仕組み

|No|処理内容 | 処理箇所 | 実装要否 | 
|:---|:-----|:-------|:--------|
|1| クライアントからサーバへコンテンツを要求。 | クライアント | 必要 | 
|2| HTTPサーバがレスポンスヘッダーに`ETag`にコンテンツのハッシュ値を付加して返す。| サーバ | 必要 | 
|3| ブラウザにてリクエスト時にリクエストヘッダーに`If-None-Match`を付加する。| クライアント | クライアントがブラウザなら不要 | 
|4| HTTPサーバにて、送信された`If-None-Match`と最新のコンテンツのハッシュ値を比較し、<br />・ 一致すれば、HTTP Status Code 304 を返却する。ボディは返却しない。<br />・ 一致しなければ、HTTP Status Code 200 および最新のコンテンツ、それに応じた`ETag`ヘッダー付加して、返却する。| サーバ | 必要 | 

## イメージ
```plantuml

autonumber

entity BrowserCache
participant Browser
participant Server

Browser -> Server: request URL(/home)
return: response, and ETag Hash
note right
コンテンツ（/home）に対するハッシュ値を計算して
ETagヘッダを付加してコンテンツを返却する。
end note

Browser -> BrowserCache: save contents and ETag

Browser -> Server: request URL(/home) and If-None-Match
Server -> Server: Check ETag Hash
note right
送られてきたIf-None-MatchのHashと
現在のコンテンツ(/home)のHashが一致するか確認する。
end note

alt Hashが一致する場合、 
Browser <- Server: HTTP Status 304, body is empty.
note right
コンテンツは更新されていないため、304を返す。
end note
BrowserCache <- Browser: get Cache Content.
note right
コンテンツは更新されていないため、
ブラウザキャッシュからコンテンツを得る。
end note
else 一致しない場合、
Browser <- Server: HTTP Status 200, recently contents.
end

```

上記の5では、いくつかの対応方法がある。
・そのときにコンテンツを計算して、それを元にHashを求める
・あらかじめコンテンツ計算して、そのHashをキャッシュしておく


## 導入方法
### WEBサーバ
1. コンテンツ(URL)ごとに`ETag`にハッシュを付加して、返すようにする。
2. コンテンツごとのハッシュをサーバ側で管理する（例：キャッシュサーバ、バックエンドサーバメモリ）
3. 上記のコンテンツごとのハッシュを更新する機構をどこかで設ける。
4. リクエストを受けた際に、`If-None-Match`ヘッダーを確認し、
    1. その値が最新のコンテンツのハッシュと同一の場合は304を返す。ボディは返さず、ブラウザキャッシュで描画させる。  
    2. 上記以外の場合は、最新のコンテンツを返す。

:::note warn
そのコンテンツがユーザ別に変わりうるのか、全ユーザ共通なのかはあらかじめ明確にすること。それによって、ハッシュ計算の対象が変わるし、全体のコストが変わる。
ユーザ別に変わる場合、そもそものキャッシュの是非、キャッシュする場合の必要容量の見積などを慎重に検討する。
:::

> 基本的には、Getメソッドが対象になるはず。
PostはURLは同一でもパラメータが異なるうえに求める結果も異なるケースがあるため、対応には精査が必要。基本しない、に寄せておくのが無難か。SSRアプリの場合は、Post&Getパターンが利用できるため、Postは対象外にしたうえで、Getにはキャッシュを活用という構えはとれる。

### ブラウザ
Cacheを有効にする。
Chromeなら、開発者ツールからDisable Cacheをオフにする。

# 作って試す
## 環境
||name |version |
|:---|:---|:---|
|OS| Ubuntu| 20.04.3 LTS |
|Server | express(NodeJS) |4.16.1 |
|Template engine | pug(NodeJS)| 2.0.0-beta11|
|Etag tool|  etag(NodeJS) | 1.8.1|
|Cache engine| 適当に手作り（５秒に１回更新）|-| 

## 実際の動き
以下は、１回目と２回目のレスポンス内容

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87538/17082cab-31cc-d949-1139-6ebd0cc48c41.png)

### 1回目
#### リクエスト
キャッシュ無し、初回、特になんの変哲もない内容。

```
GET /etag HTTP/1.1
Host: localhost:3000
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="93", " Not;A Brand";v="99", "Chromium";v="93"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
sec-ch-ua-platform: "Linux"
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:3000/etag
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Cookie: _ga=GA1.1.1911431475.1620094102; Idea-212d5d8b=f278ab51-9384-49ab-8e7b-98909abd3516; JSESSIONID=8FF8FC1D0AD60E0CB95ED1DCC6CC3269; _ga_55DN3ZP802=GS1.1.1627800089.42.0.1627800089.0; _ga_C7RLRNBP7H=GS1.1.1630922254.143.1.1630922306.0
```

#### レスポンス
##### ヘッダ
 
```
HTTP/1.1 200 OK
X-Powered-By: Express
ETag: "13-XqYPn2sNXor5g1n6J37wHO+av+k"
Content-Type: text/html; charset=utf-8
Content-Length: 1752
Date: Sun, 12 Sep 2021 10:00:08 GMT
Connection: keep-alive
```

##### ボディ
省略だが、中身はHTML。

### 2回目
#### リクエスト
`If-None-Match`を付加して送付。

```
GET /etag HTTP/1.1
Host: localhost:3000
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="93", " Not;A Brand";v="99", "Chromium";v="93"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
sec-ch-ua-platform: "Linux"
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:3000/etag
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Cookie: _ga=GA1.1.1911431475.1620094102; Idea-212d5d8b=f278ab51-9384-49ab-8e7b-98909abd3516; JSESSIONID=8FF8FC1D0AD60E0CB95ED1DCC6CC3269; _ga_55DN3ZP802=GS1.1.1627800089.42.0.1627800089.0; _ga_C7RLRNBP7H=GS1.1.1630922254.143.1.1630922306.0
If-None-Match: "13-XqYPn2sNXor5g1n6J37wHO+av+k"

```

#### レスポンス
##### ヘッダ
```
HTTP/1.1 200 OK
X-Powered-By: Express
Date: Sun, 12 Sep 2021 10:00:08 GMT
ETag: "13-XqYPn2sNXor5g1n6J37wHO+av+k"
Content-Type: text/html; charset=utf-8
Content-Length: 1752
```

##### ボディ

```

```
空。ChromeのdebugモードだとPreviewがCacheに対して効いているようで確認しづらい。
一応、レスポンスのサイズは304のときはヘッダのみのサイズで、応答速度も高速になっている。


Postmanでも念の為確認。`If-None-Match`相当のコンテンツが変わっていなければ、空と304ステータスが返るため、ブラウザキャッシュを採用する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87538/0713d361-bcb1-c57e-e093-41bf5c79fb19.png)


:::note info
ちなみにChrome側でDisable Cacheを有効にすると、`If-None-Match`は送信しない。
:::

# メリット、デメリット、その他
## メリット
* バックエンドサーバリソースの節約
* 通信の効率化

:::note warn
本当にリソースの節約となるかは要確認。
:::

## デメリット
* アーキテクチャはやや複雑化する

> 最低限、
・redisなどの外付けキャッシュサーバを頼る
・ETag用の値生成ルールはトラブルの元にならないよう確認する

## SSR（Server Side Rendering）アプリでの有効性
有用。

## CSR（Client Side Rendering）アプリでの有効性
* API利用時においては、キャッシュの面ではやや有効
    * やはりコンテンツがユーザ別かサービス共通かによって有効性が異なる
* APIコンテンツの楽観ロックにおいて非常に有効
    * フロント側の制御をブラウザに委譲できるため、バックエンドで更新対象データのハッシュ照合を行えば良い

> Service Worker のCache機構とWebWorkerによる非同期Fetchが勝りそう。

## 検討基準
以下に該当すればするほど、検討価値がある。

* 更新頻度が低い
* アクセス頻度が高い
* 通信量が多い
* リクエストあたりの処理量が多い

ホーム画面、日次更新データの一覧表示処理。

> POSTはURLで一意にならないので辞めたは方が・・・。
> 画像などの静的コンテンツはCDNで十分なケースも多そう。
> 静的コンテンツについては、きめ細かいリソース単位の更新制御の要件があるならETag活用もあるか。
> CDN + ETagの両方の活用案もある。

### fetch API を使った結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87538/52bcedb3-61ab-a693-0662-21345c13a1e4.png)

ブラウザ上で実行する限りはしっかり`ETag`周りの恩恵は受けられる。

> 関係ないが、304レスポンス時もfetchのstatusは200らしい。

# アーキテクチャ例
アーキテクチャは大別すると2パターンある

| 方式 | 効果 |
|:----|:-----|
| コンテンツの生成行い、その返却時に`ETag`のハッシュを計算し、送信されていた`If-None-Match`と比較して一致していれば、304ステータスだけ返す| 通信量の削減
| コンテンツの生成を事前に行い、最新の`ETag`と`If-None-Match`が一致していれば、304ステータスのみ返す。| ・通信量の削減<br />・レスポンス高速化|

上記の2点目のパターンを考えてみる。

```plantuml

actor Browser
rectangle  BffServer
database  CacheServer
rectangle  BackendServer
entity "cron process" as cron
entity "ETagHash\nCalculationProcess" as ETagHashCalculationProcess
Browser ---> BffServer: request
BffServer --> CacheServer: 3.check cache
BffServer --> BackendServer: 4.non cache process
ETagHashCalculationProcess --> CacheServer: 2.refresh cache
ETagHashCalculationProcess --> BackendServer: 1.pre request
cron --> ETagHashCalculationProcess: kick
```

Cache Serverにて、コンテンツ別の`ETag`値を持つようにし、
`ETag`値の設定は`ETagHashCalculationProcess`にて行う。`CacheCalculationProcess`は、不定期バッチの形態を採ったり、`BackendServer`が担ったりする。疎結合を目指すなら`BackendServer`とは別の形で用意する。

# サイト事例
[価格.com](https://kakaku.com/)
[フューチャーアーキテクト株式会社](https://www.future.co.jp/architect/)
[東京工業大学](https://www.titech.ac.jp/)

> 価格.comは、トップページが条件にかなり合致している。広告とは相性が悪いと思われるが、そこは別途取得しているようだ。

# APIでのETag事例
[Gitlab Polling API](https://docs.gitlab.com/ee/development/polling.html)

> Redisを用いたシーケンス図もあるのでオススメ

# フレームワーク事例（Spring Framework））
https://docs.spring.io/spring-framework/docs/3.0.0.M4/reference/html/ch15s11.html

> `ETag`は作って返せるが、これはレスポンス生成後に行うから。節約できるのは帯域幅だけやぞ。と注意文言。

# ミドルウェア事例
Apacheに[FileETagディレクティブ](https://httpd.apache.org/docs/2.4/ja/mod/core.html#fileetag) がある。

これが有効化されるとファイルレスポンスについて、ETagを付加して返信する。
リクエスト時もApacheで検証して、Bodyなし304レスポンスが返せる。

## 検証コード
test.png的な画像ファイルは適当に用意。

```
docker run --rm httpd:2.4 cat /usr/local/apache2/conf/httpd.conf > my-httpd.conf
echo "FileETag ALL" >> my-httpd.conf 
cat <<EOF > Dockerfile
> FROM httpd:2.4
> 
> COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
> COPY ./test.png /usr/local/apache2/htdocs/
> EOF
docker build -t httpd-etag .
docker run -p 80:80 httpd-etag

```

# クラウド事例
AWSのCloudFrontに[ETagに関する言及](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html#RequestCustomConditionalGETs)がある。

> 特に機能的なサポートはないように見える。


# 検証用コード（NodeJSサーバアプリ部分）
https://github.com/satoc0629/ExpressTest/blob/master/routes/etag.js