WordPressのページ出力にETagを追加する

##ETagとは##
**HTTPレスポンスヘッダ**のひとつで、リソースの更新状況を示す識別子です。
主にブラウザのキャッシュに利用されます。


##ETagを利用したキャッシュ動作の概要##

####クライアント(主にWebブラウザ)が対象URLのキャッシュを持っていない場合####

+ クライアントからサーバへリソース要求
+ サーバはリソースの更新状況に応じた**ETag**をレスポンスヘッダに付加してリソースを送信
+ クライアントは受け取ったリソースを表示し、**ETag**が紐付けられたキャッシュを保存

####クライアントがキャッシュを持っている場合####

+ クライアントはキャッシュに紐付く**ETag**を**If-None-Match**としてリクエストヘッダに付加した上で、サーバへリソース要求
+ サーバは受け取った**If-None-Match**の内容と該当リソースの**ETag**を比較し、一致していなければサーバ側の持つ**ETag**をレスポンスヘッダに付加してリソースを送信、一致する場合はリソースは送信せず、**HTTPステータスコード304**の**Not Modified**を返す
+ クライアントは**304 Not Modified**を受け取った場合はキャッシュの内容を表示し、リソースであればそれを表示した上で該当するキャッシュも更新する

ブラウザキャッシュに関連するHTTPヘッダは**ETag**の他にもいくつかありますが、**ETag**を利用したキャッシュは有効期限を指定するタイプのキャッシュと違い必要最小限の通信は毎回行なう反面、サーバ側でコンテンツが更新されたにもかかわらず古いキャッシュがいつまでも表示されてしまうといったトラブルも回避しやすくなります。

##WordPressをETag出力に対応させる##

`wp-blog-header.php`に対して以下の追記を行ないます。

~~~diff:wp-blog-header.php
    // Set up the WordPress query.
    wp();
+    // 出力バッファリングを有効に
+    ob_start();

    // Load the theme template.
    require_once ABSPATH . WPINC . '/template-loader.php';
+    // 出力バッファを取得し、バッファを削除
+    $obBuffer = ob_get_clean();
+
+    // ETagを生成
+    $eTag = md5($obBuffer);
+
+    // クライアントからのIf-None-Matchを取得
+    $ifNoneMatch = filter_input(INPUT_SERVER, 'HTTP_IF_NONE_MATCH');
+    // 生成したETagをIf-None-Matchと比較
+    // 圧縮転送が有効な場合サーバ側でETagに更にプレフィクスが付加され
+    // クライアントから受け取ったタグにもそれが付加されているため
+    // 完全一致ではなく、ここで生成したETagがIf-None-Matchに含まれているかで判別
+    if(strpos($ifNoneMatch, $eTag) !== false) {
+        // 一致していれば 304 Not Modified を返して終了
+        header('HTTP/1.1 304 Not Modified');
+        exit;
+    } else {
+        // クライアントからIf-None-Matchを受け取っていない、
+        // あるいはETagと異なる場合はレスポンスヘッダにETagを含めてページデータを返す
+        header(sprintf('ETag: "%s"', $eTag));
+        echo $obBuffer;
+    }

}
~~~

動作としては、送信されるべきデータを出力バッファを介して横取りし、そのデータを元に**ETag**を生成、クライアントからの**If-None-Match**の状況に応じて、通常通りデータ(と**ETag**)を送るか、未更新を意味する**304 Not Modified**のみを送るかを振り分けています。

出力バッファを横取りするのにちょうどよい位置のアクションフックが見つからなかったので、プラグインではなく`wp-blog-header.php`を直接書き換える方法をとっています。
**WordPress**の自動更新で度々上書きされるファイルでもあるので、できる限り少ない追記で対応できるようにしたつもりです。

もし他に出力バッファを扱うようなプラグイン等を利用していた場合はお互い影響してしまい正常に動作しなくなることも考えられますので、その場合はこの**ETag**出力対応の使用は中止して下さい。

##ETagが出力されているか確認##

大抵のWebブラウザにはデベロッパーツール(F12キーで起動できる場合が多い)がありますので、その中のネットワーク監視機能からそれぞれのデータのヘッダも確認することができます。
サーバから送られてくるレスポンスヘッダ内に**ETag**のフィールドがあれば送信されてきています。

`curl`コマンドの`-I`オプションでもレスポンスヘッダを確認できます。
WordPressはHEADリクエストされた場合は上記で変更を加えた部分に到達する前に処理が終了するようなので、`-X GET`オプションも付けてみてください。

###一例###

~~~plain
$ curl -I -X GET https://********.jp
HTTP/2 200
server: nginx
date: Thu, 18 Jun 2020 17:37:16 GMT
content-type: text/html; charset=UTF-8
x-powered-by: PHP/7.3.18
etag: "b3d0cb21d6686756a2f24a9311b2c467"
vary: Accept-Encoding
~~~

同じURLでありながらリクエストするたび**ETag**が変化するようであれば、そのページには毎回変化する何かが含まれているということになります。
そのようなページであっても同じ**ETag**を出してしまってはキャッシュとして不適切ですので、それは正常な動作です。

**ETag**が付加されていることが確認できたら、その**ETag**を**If-None-Match**で指定して再度リクエストしてみます。

~~~plain
$ curl -I -X GET -H "If-None-Match: "b3d0cb21d6686756a2f24a9311b2c467"" https://********.jp
HTTP/2 304
server: nginx
date: Thu, 18 Jun 2020 17:41:00 GMT
~~~

**HTTPステータスコード304**が返ってくれば、正常に動作しています。

##WordPressをETag出力に対応させたところで、どの程度恩恵があるのか##

固定ページなど、長期間にわたって内容が変化しないようなページが多ければその分ネットワークトラフィックが減りますので、それなりに恩恵はあるかと思います。

表示内容に応じた**ETag**を生成する都合上、送信する・しないに関わらずリソース(ページデータ)自体は生成しますので、サーバ側の負荷という点で見ればリソース分のトラフィックが節約できる以外はほとんど変わりはありませんが、ネットワークトラフィックの節約はとくに転送データ量に制限のあるモバイル通信などでは有用なのではないかと思います。

但し、ページ内容自体は変わっていないように見えても、そのページ内に例えばランダムに変わるおすすめページのピックアップや、コメントフォームにリロードのたびに内容の変わる認証コード系の隠しフィールドなどがあったりしてHTMLソースレベルでの変化があれば**ETag**も異なるものとなるため、その場合はキャッシュとしての動作は望めません。
**ETag**を使うことで恩恵のあるページがどの程度あるかを見極めながら利用を検討してください。


##参考##
[HTTPヘッダー | MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers)
[HTTPキャッシュ | MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Caching)
[ETag | MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/ETag)