ウェブサーバは、キャッシュに関する情報をレスポンスヘッダに含めることで、そのレスポンスで返したリソースをどのようにキャッシュすべきかをウェブクライアントに指示することができる。ウェブクライアントはその指示に基づいた処理を行う。
この記事では、キャッシュの制御に使われる HTTP ヘッダについて見ていく。
動作確認に使った環境は以下の通り。
- ウェブサーバ
- Deno 1.4.4
- ウェブクライアント
- Google Chrome 86.0.4240.75
Last-Modified と If-Modified-Since
Last-Modified
は、当該リソースの最終更新日時を伝えるためのヘッダで、値として以下のようなRFC-1123
形式の日時を設定する。
Wed, 14 Oct 2020 15:00:00 GMT
レスポンスヘッダにLast-Modified
フィールドを含めると、次にそのリソースに対してリクエストを発行した際に、リクエストヘッダにIf-Modified-Since
が含まれるようになる。
例えば、以下のサーバを起動した状態でhttp://localhost:8080/cache
にアクセスすると、レスポンスヘッダにlast-modified: Wed, 14 Oct 2020 15:00:00 GMT
が含まれる。
そして、2 回目以降のアクセスでは、リクエストヘッダにIf-Modified-Since: Wed, 14 Oct 2020 15:00:00 GMT
が含まれるようになる。
http://localhost:8080/
ではlast-modified
を返していないので、何度アクセスしてもリクエストヘッダにIf-Modified-Since
が含まれることはない。
import { listenAndServe, ServerRequest, } from "https://deno.land/std@0.74.0/http/mod.ts"; listenAndServe({ port: 8080 }, async (req: ServerRequest) => { const body = `<html> <head></head> <body> <p>a</p> <p> <a href="/">/</a> </p> <p> <a href="/cache">/cache</a> </p> </body> </html>`; switch (req.url) { case "/": { req.respond({ status: 200, headers: new Headers({ "Content-Type": "text/html", }), body, }); break; } case "/cache": { req.respond({ status: 200, headers: new Headers({ "Content-Type": "text/html", "Last-Modified": new Date(2020, 9, 15).toUTCString(), }), body, }); break; } default: req.respond({ status: 404, headers: new Headers({ "Content-Type": "text/plain", }), body: "Not found\n", }); break; } });
今回は実装していないが、リクエストヘッダのIf-Modified-Since
の日時とサーバ側が持っている「最終更新日時」を比較し、リソースの更新がなければステータスコード304
を返してキャッシュを使わせる、ということが可能になる。
ETag と If-None-Match
Last-Modified
はリソースの更新日時でキャッシュを利用すべきか判断するが、ETag
はリソースの中身でキャッシュを利用すべきか判断する。
具体的には、リソースのハッシュ値などをETag
フィールドの値として設定する。
以下のコードでは、レスポンスボディの MD5 ハッシュ値を、ETag
の値として使っている。
import { listenAndServe, ServerRequest, } from "https://deno.land/std@0.74.0/http/mod.ts"; import { createHash } from "https://deno.land/std@0.74.0/hash/mod.ts"; listenAndServe({ port: 8080 }, async (req: ServerRequest) => { const body = `<html> <head></head> <body> <p>a</p> <p> <a href="/">/</a> </p> <p> <a href="/cache">/cache</a> </p> </body> </html>`; switch (req.url) { case "/": { req.respond({ status: 200, headers: new Headers({ "Content-Type": "text/html", }), body, }); break; } case "/cache": { const md5 = (value: string) => { const hash = createHash("md5"); hash.update(value); return hash.toString(); }; req.respond({ status: 200, headers: new Headers({ "Content-Type": "text/html", "ETag": md5(body), }), body, }); break; } default: req.respond({ status: 404, headers: new Headers({ "Content-Type": "text/plain", }), body: "Not found\n", }); break; } });
http://localhost:8080/cache
にアクセスすると、レスポンスヘッダにetag: 434bb620aed649cf07bac5554569f60f
が含まれる。
そして、2 回目以降のアクセスでは、リクエストヘッダにIf-None-Match: 434bb620aed649cf07bac5554569f60f
が含まれるようになる。
リソースが変わらない限りはETag
の値も変わらないので、ETag
が変わらない限りはキャッシュを使わせる、ということが可能になる。
先程のコードに以下の変更を行い、実装してみる。
hash.update(value); return hash.toString(); }; - req.respond({ - status: 200, - headers: new Headers({ - "Content-Type": "text/html", - "ETag": md5(body), - }), - body, - }); + const isValidCache = (ifNoneMatch: string | null) => { + if (ifNoneMatch === null) return false; + return ifNoneMatch === md5(body); + }; + if (isValidCache(req.headers.get("if-none-match"))) { + req.respond({ + status: 304, + headers: new Headers({ + "Content-Type": "text/html", + }), + }); + } else { + req.respond({ + status: 200, + headers: new Headers({ + "Content-Type": "text/html", + "ETag": md5(body), + }), + body, + }); + } break; } default:
http://localhost:8080/cache
への初回アクセスではステータスコード200
を返すが、それ以降は304
を返し、ブラウザにキャッシュを使わせるようになる。
<p>a</p>
の部分を<p>b</p>
に変えると、isValidCache
がfalse
を返すようになるため、最新のリソースをステータスコード200
で返す。
そしてまた、リソースが更新されない限りは304
を返すようになる。
Expires
ここまでの例では、キャッシュを使う場合も HTTP リクエスト自体は発生していた。
Expires
フィールドを使うと、HTTP リクエストを抑制できる。
このフィールドを使って有効期限を設定することで、その期限が切れるまではクライアントは HTTP リクエスト自体を行わず、キャッシュを使い続けるようになる。
値のフォーマットは、Last-Modified
と同様に、RFC-1123
形式。
以下のコードでは、Expires
として、サーバへのアクセスがあった20
秒後の時間を設定している。
そのため、http://localhost:8080/cache
にアクセスしてから20
秒間は、何度http://localhost:8080/cache
にアクセスしても、HTTP リクエストが発行されない。
サーバへのリクエストそのものが発生していないため、例えリソースが更新されたとしてもそれに気付くことができない。そのため、Expires
以降にアクセスを行ってリクエストが発生するまでは、キャッシュされた古いリソースを使い続けることになる。
import { listenAndServe, ServerRequest, } from "https://deno.land/std@0.74.0/http/mod.ts"; listenAndServe({ port: 8080 }, async (req: ServerRequest) => { const body = `<html> <head></head> <body> <p>a</p> <p> <a href="/">/</a> </p> <p> <a href="/cache">/cache</a> </p> </body> </html>`; switch (req.url) { case "/": { req.respond({ status: 200, headers: new Headers({ "Content-Type": "text/html", }), body, }); break; } case "/cache": { const now = new Date(); now.setSeconds(now.getSeconds() + 20); req.respond({ status: 200, headers: new Headers({ "Content-Type": "text/html", "Expires": now.toUTCString(), }), body, }); break; } default: req.respond({ status: 404, headers: new Headers({ "Content-Type": "text/plain", }), body: "Not found\n", }); break; } });
ここまで紹介してきたLast-Modified
やETag
と組み合わせて使うことも可能。
その場合、Expires
が切れるまではキャッシュを使い続け、それ以降のアクセスについては、サーバがリソースの更新日時やハッシュ値を検証して、キャッシュを使うかどうかクライアントに指示を出すことになる。
Cache-Control
Cache-Control
による指示は強力で、ここまで紹介してきた機能よりも優先される。
例えば、"Cache-Control": "no-store"
は「キャッシュを利用しない」を意味しており、この指定がある場合、キャッシュを利用させようとする他の指示は無効化される。
先程の例に"Cache-Control": "no-store"
を追加してみる。
headers: new Headers({
"Content-Type": "text/html",
"Expires": now.toUTCString(),
+ "Cache-Control": "no-store",
}),
body,
Expires
で「20
秒間は HTTP リクエストを行わずキャッシュを使う」と指示しているが、それは無効化され、アクセスの度に HTTP リクエストが発行される。
そしてno-store
は、Last-Modified
やETag
も無効化する。
具体的には、レスポンスヘッダにLast-Modified
やETag
を含めても、次回以降のリクエストヘッダにIf-Modified-Since
もIf-None-Match
も含まれなくなる。
そのため、常に初回アクセスと同様に扱われ、キャッシュが使われない。
Cache-Control
には他に、no-cache
やmax-age=n
などを指定することができる。
no-cache
は、常に HTTP リクエストを発生させ、キャッシュを使うかどうかをサーバに判断させる。
こちらはIf-Modified-Since
やIf-None-Match
は消されないため、これらの値を使って、キャッシュを使うかどうかをサーバに判断させることができる。
max-age=n
は、n
秒間は HTTP リクエストを行わず、キャッシュを使う。それ以降はIf-Modified-Since
やIf-None-Match
を使って、キャッシュを使うかどうかサーバが判断する。
no-cache
やmax-age=n
でもExpires
は無効化される。
Expires
の値がなんであれ、no-cache
の場合は常に HTTP リクエストが発行されるし、max-age=n
の場合はExpires
の日時ではなく「n
秒間はキャッシュを使う」という指定が採用される。