Cloudflare の 499 Client Close Request を再現する

# `499 Client Close Request` とは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/78f5c647-22b8-5a3e-75af-7f9cce07806e.png)

- [4xx Client Error · Cloudflare Support docs](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/4xx-client-error/#499-client-close-request)

> nginx 特有のレスポンスコードで、サーバーがリクエストの処理中にクライアントによってコネクションが閉じられ、サーバーがステータスコードを送り返せなくなったことを示します。

Cloudflare は nginx / [Pingora](https://blog.cloudflare.com/how-we-built-pingora-the-proxy-that-connects-cloudflare-to-the-internet/) を使ってプロキシしていますが、そのプロキシ処理の途中でクライアントから何らかの理由で接続が切断されると、`499 Client Close Request` となります。 

あくまで `499` のステータスコードがログとして残るだけで、クライアントにレスポンスを返しているわけではありません。

```mermaid:
---
title: 499 Client Close Request
---
graph LR
Client <--> |何らかの理由で切断|Cloudflare[Cloudflare<br><br>499 ログだけ残る] <--> |何かやりとりしている途中|Origin
```

Cloudflare から Origin へ接続するときには、TCP 3ウェイハンドシェイク処理のタイムアウトが 30 秒のため、以下の 3 パターンが考えられます。

# 1. クライアントのタイムアウトが短い場合
>クライアントのタイムアウトが短い場合（30秒未満）、クライアントは接続をあきらめるため、Cloudflareは499エラーをログに記録します。

## 1-1. めちゃ短い
例えば、以下のように `curl -m` でタイムアウト値を短く指定することで再現できます。

実際に 0.04 秒のタイムアウト値でリクエストすることはほとんどないため、クライアントの接続状況によって 499 ログが残ることがあっても、特に気にするほどの問題ではないケースがあります。

```shell:
% while true; do sleep 1 && curl -m 0.04 https://example.com/; done
curl: (28) Operation timed out after 45 milliseconds with 0 bytes received
curl: (28) Operation timed out after 43 milliseconds with 0 bytes received
curl: (28) Operation timed out after 41 milliseconds with 0 bytes received
curl: (28) Operation timed out after 40 milliseconds with 0 bytes received
```
## 1-2. TCP 接続タイムアウトより短い

または Origin をファイアウォールでブロックする等で、TCP 接続自体を妨害した上で 30 秒未満のタイムアウト値を使うことでも再現できます。

TCP 3ウェイハンドシェイク処理のタイムアウト 30 秒を待つ間にクライアントから切断されるケースです。

```shell:
% while true; do sleep 1 && curl -m 29 https://example.com/; done
curl: (28) Operation timed out after 29005 milliseconds with 0 bytes received
curl: (28) Operation timed out after 29002 milliseconds with 0 bytes received
curl: (28) Operation timed out after 29005 milliseconds with 0 bytes received
curl: (28) Operation timed out after 29002 milliseconds with 0 bytes received
```

## 1-3. HTTP タイムアウトより短い

以下の HTTP タイムアウトを待たずに、クライアントから接続を切断したケースです。

- [Error 524: a timeout occurred · Cloudflare Support docs](https://developers.cloudflare.com/support/troubleshooting/cloudflare-errors/troubleshooting-cloudflare-5xx-errors/#error-524-a-timeout-occurred)

>エラー524は、CloudflareがオリジンWebサーバーへのTCP接続に成功したことを示しますが、接続がタイムアウトする前に、オリジンがHTTPレスポンスで応答しなかったということです。通常、CloudflareはHTTPレスポンスを100秒待ちます。その時間内にオリジンが応答しない場合、Cloudflareは接続を閉じ、「エラー524: A timeout occurred (タイムアウトが発生しました)」と表示されます。

Apache + PHP の構成では、以下の設定により再現できます。

```conf:httpd.conf
Timeout 120
```

```php:524.html
<!DOCTYPE html>
<?php
  sleep(120);
  echo "WOW, you waited me for 120 seconds.";
?>
</html>
```

以下のコマンドでは 100 秒待った結果、524 レスポンスが得られています。

```shell:
% curl -w" time_total: %{time_total}\n" https://example.com/524.html
error code: 524 time_total: 100.115141
```

その状態で 100 秒より短いタイムアウトにより、クライアントから接続を切断すると 499 となります。

```shell:
% curl -m 30 https://example.com/524.html                          
curl: (28) Operation timed out after 30004 milliseconds with 0 bytes received 
```

# 2. クライアントのタイムアウトが長い場合 (`200`)

>クライアントのタイムアウトが長い場合（30秒以上）、TCP接続が正常に確立されると、HTTPトランザクションは通常通り継続されます。この場合、Cloudflareは通常のステータスコード（HTTP 200）を返します。

通常通り、処理される場合になります。

```shell
% curl -m 300 https://example.com/ | head -n 1
HTTP/2 200 
```

# 3. クライアントのタイムアウトが長い場合 (`522`)

>クライアントがそれ以上のタイムアウトを持ち、CloudflareがOriginサーバーとのTCPハンドシェイクを確立できなかった場合、HTTP 522を返します。

Origin をファイアウォールでブロックする等で、TCP 接続自体を妨害した上で 30 秒以上のタイムアウト値を使うことで再現できます。

TCP 3 ウェイハンドシェイク処理のタイムアウト 30 秒を待ち、`522 Origin Connection Time-out` レスポンスを確認できます。

```shell:
% curl -m 300 https://example.com/
error code: 522
```

# まとめ

あくまでいくつかの例で 499 を再現できましたが、他の 5XX エラーでも同様に再現できると思います。
Cloudflare 内部エラーを伴うケースもないとは言えないため、トラブルシューティングには Cloudflare サポートの協力を仰ぐようにしてください。
また、オリジン側のログも突き合わせてみることで、通信全体のどこでタイムアウト・エラー等が発生しているのかがより詳細に確認できると思います。