Cloudflare でステータスコードごとの Edge Cache TTL を確認する

# ステータスコードごとの Edge Cache TTL
Cloudflare では、ステータスコードごとの Edge Cache TTL を設定できます。

- [Cache by status code · Cloudflare Cache (CDN) docs](https://developers.cloudflare.com/cache/how-to/configure-cache-status-code/)
> デフォルトでは、Cloudflare は cache-control ディレクティブや expires レスポンスヘッダが存在しない場合、特定の HTTP レスポンスコードを以下の Edge Cache TTL でキャッシュします。 
> | HTTP status code   | Default TTL |
> | ------------------ | ----------- |
> | 200, 206, 301      | 120m        |
> | 302, 303           | 20m         |
> | 404, 410           | 3m          |
> | 403                | 0s          |
> | 500, 502, 503, 504 | 0s          |

Default TTL = 0s の場合は、デフォルトではリソースをキャッシュしない形になります。

- [Default Cache Behavior · Cloudflare Cache (CDN) docs](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/)
> Cloudflareは、Edge Cache TTLページルールがヘッダを上書きしない限り、オリジンウェブサーバのキャッシュヘッダを以下の順序で尊重します。
Cloudflareは以下の場合、リソースをキャッシュしません：
Cache-Control ヘッダが private、no-store、no-cache、または max-age=0 に設定されている場合。


# オリジンの準備
以下の要領でオリジンサーバーを準備します。

- [Cloudflare からの HTTP リクエストヘッダを可視化する - Qiita](https://qiita.com/khayama/items/0787728497918f2e68d6)

その上で特定のステータスコードを返す HTML ページを用意します。

- [How To Return Status Codes In PHP - SISTRIX](https://www.sistrix.com/ask-sistrix/onpage-optimisation/http-status-code/return-status-codes-in-php)

```html:400.html
<?php http_response_code(400);  ?>
<!DOCTYPE html>
<html>

<head>
    <title>php-generated 400</title>
</head>

<body>
    This is a 400 error page.
</body>

</html>
```

# `.html` をキャッシュする設定
- [Default Cache Behavior · Cloudflare Cache (CDN) docs](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#default-cached-file-extensions)

HTML はデフォルトでキャッシュされないため、Cache Rules を使ってキャッシュする設定にしておきます。

- When incoming requests match…
    - `(http.request.uri.path.extension eq "html")`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/d90af4ca-de2b-c213-126d-ab69a9023fa5.png)

# Status code TTL 設定
デフォルト設定から変更したい場合、Cache Rules を使って、例えば以下のように設定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/af8d29bb-1303-5fd4-eaba-4d934300d629.png)


# 400 で確認

```shell:
while true; do curl -sIXGET https://example.com/400.html | grep -e cf-cache-status: -e age: -e HTTP -e cache-control: && sleep 3; done
```

## デフォルト = 0s

```shell:
HTTP/2 400 
cf-cache-status: MISS
HTTP/2 400 
cf-cache-status: MISS
HTTP/2 400 
cf-cache-status: MISS
```

## Status code TTL = 30s
Status code TTL 設定が反映されたことがわかります。

```shell:
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: EXPIRED
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 3
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 6
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: HIT
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 3
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: HIT
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 9
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 12
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 25
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: EXPIRED
HTTP/2 400 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 6
```

# 403 で確認

```shell:
while true; do curl -sIXGET https://example.com/403.html | grep -e cf-cache-status: -e age: -e HTTP -e cache-control: && sleep 3; done
```

(`.htaccess ` などのパスを使って確認するのもありです。）

## デフォルト = 0s

```shell:
HTTP/2 403 
cf-cache-status: MISS
HTTP/2 403 
cf-cache-status: MISS
HTTP/2 403 
cf-cache-status: MISS
```

## Status code TTL = 30s
Status code TTL 設定が反映されたことがわかります。

```shell:
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: MISS
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 3
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 6
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 9
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 12
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 15
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 21
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 24
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 28
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: EXPIRED
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
HTTP/2 403 
cache-control: max-age=31536000
cf-cache-status: HIT
age: 6
```

# 404 で確認

```shell:
while true; do curl -sIXGET https://example.com/404.html | grep -e cf-cache-status: -e age: -e HTTP -e cache-control: && sleep 3; done
```

(実際には存在しないパスを使って確認するのもありです。）

## デフォルト = 180s

```shell:
HTTP/2 404 
cf-cache-status: MISS
cache-control: public, max-age=14400
HTTP/2 404 
cf-cache-status: HIT
cache-control: public, max-age=14400
HTTP/2 404 
cf-cache-status: HIT
age: 7
cache-control: public, max-age=14400
...
HTTP/2 404 
cf-cache-status: HIT
age: 174
cache-control: public, max-age=14400
HTTP/2 404 
cf-cache-status: EXPIRED
cache-control: public, max-age=14400
HTTP/2 404 
cf-cache-status: HIT
age: 174
cache-control: public, max-age=14400
HTTP/2 404 
cf-cache-status: HIT
age: 9
cache-control: public, max-age=14400
HTTP/2 404 
cf-cache-status: HIT
age: 10
cache-control: public, max-age=14400
```

## Status code TTL = -1 (no store)
Status code TTL 設定が反映されたことがわかります。

```shell:
HTTP/2 404 
cache-control: public, max-age=31536000
cf-cache-status: MISS
HTTP/2 404 
cache-control: public, max-age=31536000
cf-cache-status: MISS
HTTP/2 404 
cache-control: public, max-age=31536000
cf-cache-status: MISS
```

# 502 で確認

```shell:
while true; do curl -sIXGET https://example.com/502.html | grep -e cf-cache-status: -e age: -e HTTP -e cache-control: && sleep 3; done
```

## デフォルト = 0s

```shell:
HTTP/2 502 
cf-cache-status: MISS
HTTP/2 502 
cf-cache-status: MISS
HTTP/2 502 
cf-cache-status: MISS
```

## Status code TTL = 1s
`STALE` 状態で返してしまいます。

```
HTTP/2 502 
cache-control: max-age=31536000
cf-cache-status: MISS
HTTP/2 502 
cache-control: public, max-age=0
cf-cache-status: STALE
age: 3
HTTP/2 502 
cache-control: public, max-age=0
cf-cache-status: STALE
age: 6
HTTP/2 502 
cache-control: max-age=31536000
cf-cache-status: STALE
HTTP/2 502 
cache-control: public, max-age=0
cf-cache-status: STALE
age: 12
```

Amazon CloudFront の場合、以下のような挙動がドキュメントで公開されていますが、Cloudflare でも同様の挙動となっています。

> - [コンテンツがキャッシュに保持される期間 (有効期限) の管理 - Amazon CloudFront](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Expiration.html)
> オリジンが接続不能で、さらに最小 TTL が 0 より大きい場合、CloudFront は、先にオリジンから取得済みのオブジェクトを返信します。この動作を回避するには、Cache-Control: stale-if-error=0 ディレクティブに、オリジンから返されたオブジェクトを含めます。このようにすることで、オリジンが接続不能な場合に CloudFront が以後のリクエストに応答する際、以前にオリジンから取得したオブジェクトを返すのではなくエラーを返すようになります。

> - [Origin Cache Control · Cloudflare Cache (CDN) docs](https://developers.cloudflare.com/cache/concepts/cache-control/)
`stale-if-error=seconds` — Indicates that when an error is encountered, a cached stale response may be used to satisfy the request, regardless of other freshness information. To avoid this behavior, include `stale-if-error=0` directive with the object returned from the origin. 

## Status code TTL = 1s + `stale-if-error=0`
- [RFC 5861 - HTTP Cache-Control Extensions for Stale Content](https://datatracker.ietf.org/doc/html/rfc5861#section-4)

`stale-if-error` ディレクティブを使うことで、オリジンがエラーステータス（ステータスコード 500, 502, 503, 504）を応答する場合に一定期間は古いキャッシュを使用できますが、 `STALE` を使用したくない場合にはオリジン側で `stale-if-error=0` を明示的に追加して無効化することで意図した通りの挙動にできました。

```shell:
HTTP/2 502 
cache-control: max-age=315360000, public, stale-if-error=0
cf-cache-status: EXPIRED
HTTP/2 502 
cache-control: max-age=315360000, public, stale-if-error=0
cf-cache-status: HIT
age: 1
HTTP/2 502 
cache-control: max-age=315360000, public, stale-if-error=0
cf-cache-status: EXPIRED
HTTP/2 502 
cache-control: max-age=315360000, public, stale-if-error=0
cf-cache-status: HIT
HTTP/2 502 
cache-control: max-age=315360000, public, stale-if-error=0
cf-cache-status: EXPIRED
HTTP/2 502 
cache-control: max-age=315360000, public, stale-if-error=0
cf-cache-status: HIT
age: 1
```

# まとめ

Cloudflare でステータスコードごとの Edge Cache TTL を確認することができました。
いくつか簡単なページを用意して意図した挙動を再現させることができれば、自信を持ってキャッシュ実装に進めると思います。

# 参考 : Apache オリジンでの Cache-Control ヘッダ追加

```conf:/etc/httpd/conf/httpd.conf
Header always append Cache-Control "max-age=315360000, public, stale-if-error=0"
# Header always append Cache-Control "max-age=31536000"
# Header always append Cache-Control "max-age=0"
```

# 参考 : Single File Purge

https://gist.github.com/kyouheicf/2d962c77869a12d413edb2f298fd20ff

# 参考 : Purge Everything

https://gist.github.com/kyouheicf/9b59f2eaca06bdbe8bbbc1bd6b74d260