地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【nginx】proxy_ignore_client_abortとか499の話

HTTP 499 エラーはクライアントがHTTPリクエストを送った後にレスポンスを待たずに切断(FIN)を送った場合にnginxがログに出力するステータス。

実験

以下のように意図的に30秒くらい時間がかかるwebサーバを用意しておく

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("recv")
    hello := []byte("Hello World!!!")
    time.Sleep(time.Second * 5)
    _, err := w.Write(hello)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("end")
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    fmt.Println("Server Start Up........")
    log.Fatal(http.ListenAndServe("localhost:8081", nil))
}

nginxを用意する。proxy_passさえ用意されていればなんでもよい

worker_processes  1;

events {
    worker_connections  1024;
}


http {
    default_type  application/octet-stream;
    sendfile        on;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;

    keepalive_timeout  65;
    server {
        listen       80 reuseport;
        server_name  localhost;

        location / {
        proxy_ignore_client_abort off;
            proxy_pass   http://127.0.0.1:8081;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

これに対してcurlを実行してsleepしてる最中にcurlを止めてみる

$ curl localhost/hello

そうするとnginxには499のログが出力される

127.0.0.1 - - [23/Feb/2023:12:56:35 +0900] "GET /hello HTTP/1.1" 499 0 "-" "curl/7.81.0" "-"

この時にnginxでは何が起きているのかを見ていく。upstream、クライアントとの通信はngx_http_upstream_check_broken_connectionで管理している。ここでは最後にクライアントとのコネクションが繋がっているかを確認していて切断済みならngx_http_upstream_finalize_requestを呼び出す。どうやって実現しているかというとnginxが使っているepoll_wait(2)がTCP でピアがシャットダウンしたことを検出したかどうかで判断している。ユーザーとのコネクションがない状態でupstreamとコネクションがつながっている状態=499となる

static void
ngx_http_upstream_check_broken_connection(ngx_http_request_t *r,
    ngx_event_t *ev)

    if ((ngx_event_flags & NGX_USE_EPOLL_EVENT) && ngx_use_epoll_rdhup) {
        if (!u->cacheable && u->peer.connection) { // ユーザーとのコネクションが
            ngx_log_error(NGX_LOG_INFO, ev->log, err,
                        "epoll_wait() reported that client prematurely closed "
                        "connection, so upstream connection is closed too");
            ngx_http_upstream_finalize_request(r, u,
                                               NGX_HTTP_CLIENT_CLOSED_REQUEST); 
            return;
        }

ngx_http_upstream_finalize_requestではいろいろじょうけんを見つつupstreamとの接続を切断する処理となっている。

github.com

static void
ngx_http_upstream_finalize_request(ngx_http_request_t *r,
    ngx_http_upstream_t *u, ngx_int_t rc)
{
        ngx_close_connection(u->peer.connection);

これだけをみるとわかるがupstreamにはTCP-FINが飛んでくるだけになる。つまり受信側の切断検出は、recv()がlength==0で返ってくるかなどを見てあげる必要がある。普通にアプリケーション書いてるだけでは実はアプリケーションは継続を続ける(はず)。今回の場合はFINで閉じるのでsend(2)してれば2回目で気づける。(send()自体はカーネルの送信バッファにデータコピー)

あとはEPOLLRDHUPというフラグをepollで検出するようにアプリケーションを実装するとよさそう。webアプリケーションがそんな実装をしているケースはあるのだろうか?ちょっと調べてみたがなさそうだったのでどうなんだろう?という気持ち

ymmt.hatenablog.com