Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13197] ファイルの概要

このコミットは、Go言語のnet/httpパッケージにおけるサーバーの応答処理に関する重要な改善を導入しています。具体的には、クライアントがリクエストボディを完全に送信する前にサーバーが応答を返す場合に、TCP接続が予期せずリセット(RST)されるのを防ぐための変更です。これにより、クライアントが応答ボディを適切に受信できるようになり、より堅牢なHTTP通信が実現されます。

コミット

commit 12b2022a3b20565c0c995f86de4f072964679047
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue May 29 12:40:13 2012 -0700

    net/http: flush server response gracefully when ignoring request body
    
    This prevents clients from seeing RSTs and missing the response
    body.
    
    TCP stacks vary. The included test failed on Darwin before but
    passed on Linux.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6256066

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/12b2022a3b20565c0c995f86de4f072964679047

元コミット内容

net/http: flush server response gracefully when ignoring request body

このコミットは、サーバーがリクエストボディを無視する際に、応答を優雅にフラッシュするようにします。これにより、クライアントがRST(リセット)を受信したり、応答ボディを見逃したりするのを防ぎます。TCPスタックは様々であり、この変更に含まれるテストは以前はDarwinで失敗しましたが、Linuxでは成功していました。

変更の背景

この変更の背景には、HTTPサーバーがクライアントからのリクエストボディを完全に読み込む前に、何らかの理由で応答を返す場合に発生する問題があります。例えば、サーバーがリクエストヘッダーを解析した時点で、認証エラー(401 Unauthorized)などのエラー応答を即座に返す必要がある場合が考えられます。

従来の挙動では、サーバーがリクエストボディの残りを読み込まずに接続を閉じようとすると、TCPスタックによっては、クライアントに対してRST(Reset)パケットを送信してしまう可能性がありました。RSTは、通常、予期せぬエラーや接続の強制終了を示すものであり、クライアント側では応答が途中で切断されたと認識され、応答ボディを完全に受信できない、あるいは接続エラーとして処理されるといった問題を引き起こします。

特に、異なるオペレーティングシステム(例: DarwinとLinux)のTCPスタックの実装の違いにより、この問題の発生状況が異なっていたことが、コミットメッセージから読み取れます。Darwin(macOS)ではRSTが発生しやすかったのに対し、Linuxでは発生しにくかったようです。この挙動の不一致は、クロスプラットフォームでのアプリケーションの安定性に影響を与えるため、Goの標準ライブラリとしてこの問題を解決する必要がありました。

この問題は、GoのIssueトラッカーでhttp://golang.org/issue/3595として報告されており、このコミットはその解決策として導入されました。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念を把握しておく必要があります。

  • HTTPプロトコル:
    • リクエスト/レスポンス: クライアントがリクエストを送信し、サーバーがレスポンスを返すという基本的なHTTPの通信モデル。
    • リクエストボディ: POSTリクエストなどでクライアントがサーバーに送信するデータ本体。
    • レスポンスボディ: サーバーがクライアントに返すデータ本体。
    • Content-Lengthヘッダー: リクエストまたはレスポンスボディの長さをバイト単位で示すHTTPヘッダー。
    • HTTP/1.1の持続的接続 (Persistent Connections): 一度確立されたTCP接続を複数のHTTPリクエスト/レスポンスで再利用する仕組み。これにより、接続確立のオーバーヘッドが削減される。
  • TCPプロトコル:
    • TCP接続: クライアントとサーバー間で信頼性の高いデータ転送を保証する接続指向のプロトコル。
    • FIN (Finish) パケット: TCP接続の正常な終了を示すパケット。片方向のデータ送信が終了したことを通知する。
    • RST (Reset) パケット: TCP接続の強制終了を示すパケット。通常、エラー状態や予期せぬ切断が発生した場合に送信される。RSTを受信した側は、通常、その接続が突然切断されたと認識する。
    • TCPバッファ: 送信または受信されるデータを一時的に保持するメモリ領域。
    • Flush操作: バッファに蓄積されたデータを強制的に下位層(この場合はTCPソケット)に書き出す操作。
    • CloseWrite(): TCP接続において、送信側がこれ以上データを送信しないことを示すためにFINパケットを送信する操作。これにより、接続の送信側が閉じられるが、受信側はまだ開いている状態になる。
  • Go言語のnet/httpパッケージ:
    • Go言語でHTTPクライアントおよびサーバーを実装するための標準ライブラリ。
    • http.ResponseWriter: HTTPレスポンスを書き込むためのインターフェース。
    • http.Request: 受信したHTTPリクエストを表す構造体。
    • http.Server: HTTPサーバーの機能を提供する構造体。
    • conn構造体: net/httpパッケージ内部でTCP接続を管理するための構造体。
    • requestBodyLimitHitフラグ: サーバーがリクエストボディの読み込みを途中で停止したことを示す内部フラグ。例えば、MaxBytesReaderで設定された最大サイズを超過した場合などに設定される。

技術的詳細

このコミットの核心は、HTTPサーバーがクライアントのリクエストボディを完全に読み込む前に応答を返す際に、TCP接続がRSTされるのを防ぐための「優雅なクローズ(graceful close)」処理の導入です。

問題のシナリオは以下の通りです。

  1. クライアントが大きなリクエストボディを持つHTTP POSTリクエストを開始する。
  2. サーバーはリクエストヘッダーを受信し、処理を開始する。
  3. サーバーは、リクエストボディの残りを読み込む前に、何らかの理由(例: 認証失敗、不正なヘッダーなど)でエラー応答(例: 401 Unauthorized)をクライアントに返す必要があると判断する。
  4. サーバーは応答をクライアントに送信するが、クライアントはまだリクエストボディの送信を完了していない。
  5. サーバーがTCP接続を即座に閉じようとすると、クライアントのTCPスタックがまだデータを送信しようとしている状態であるため、サーバーのOSがRSTパケットをクライアントに送信してしまう可能性がある。
  6. クライアントはRSTを受信し、応答ボディを完全に読み込む前に接続が強制終了されたと認識する。

このコミットは、この問題を解決するために以下の変更を導入しています。

  1. requestBodyLimitHitフラグの利用: response構造体(内部的にはhttp.ResponseWriterの実装)に存在するrequestBodyLimitHitという内部フラグが、この新しい挙動のトリガーとなります。このフラグは、サーバーがリクエストボディの読み込みを途中で停止した場合(例えば、MaxBytesReaderによって設定された制限を超過した場合や、エラー応答を返すために意図的に読み込みを停止した場合)に設定されます。

  2. conn.closeWrite()メソッドの導入: conn構造体(TCP接続をラップする内部構造体)にcloseWrite()という新しいメソッドが追加されました。このメソッドは以下の処理を行います。

    • c.finalFlush()を呼び出し、送信バッファに残っているすべてのデータをTCPソケットにフラッシュします。
    • 基盤となるnet.TCPConnに対してCloseWrite()を呼び出します。CloseWrite()は、TCP接続の送信側を閉じ、FINパケットをピア(クライアント)に送信します。これにより、サーバーはこれ以上データを送信しないことをクライアントに通知します。しかし、接続の受信側はまだ開いているため、クライアントはサーバーからの応答を読み続けることができます。
  3. 応答後の遅延クローズ: conn.serve()メソッド内で、HTTPリクエストの処理が完了し、w.closeAfterReplytrue(つまり、応答後に接続を閉じる必要がある場合)かつw.requestBodyLimitHittrueの場合に、特別な処理が追加されました。

    • まず、c.closeWrite()が呼び出され、サーバーの応答がフラッシュされ、FINパケットがクライアントに送信されます。
    • 次に、time.Sleep(250 * time.Millisecond)が実行されます。これは、サーバーが接続を完全に閉じる前に、250ミリ秒間待機するというものです。この短い遅延は、クライアントがサーバーから送信されたFINパケットを受信し、応答ボディを完全に読み込むための時間を与えます。この間にクライアントが応答を読み終えれば、サーバーが最終的に接続を閉じても、クライアントはRSTではなく正常なFIN/ACKシーケンスとして処理できます。250ミリ秒という値は、コミットメッセージによると「いくぶん恣意的」ですが、地球の半周分のレイテンシを考慮した上で、おそらく1秒全体を待つ必要はないという判断に基づいています。

このメカニズムにより、サーバーはリクエストボディの読み込みを途中で停止した場合でも、クライアントに対してRSTを送信することなく、応答を確実に届け、接続を優雅に終了させることが可能になります。

コアとなるコードの変更箇所

src/pkg/net/http/serve_test.go

新しいテストケース TestServerGracefulClose が追加されました。 このテストは、以下のシナリオをシミュレートします。

  1. httptest.NewServer を使用してテスト用のHTTPサーバーを起動します。このサーバーは、どんなリクエストに対しても即座に 401 Unauthorized エラーを返します。
  2. クライアント側で、非常に大きな Content-Length (5MB) を持つPOSTリクエストを作成し、サーバーに送信を開始します。
  3. クライアントはリクエストボディの送信をバックグラウンドのゴルーチンで行い、同時にサーバーからの応答を読み取ります。
  4. サーバーはリクエストボディを完全に受信する前に 401 Unauthorized 応答を返します。
  5. テストは、クライアントが 401 Unauthorized 応答を正常に受信できることを確認します。以前の挙動では、この時点でRSTが発生し、クライアントが応答を読み取れない可能性がありました。
  6. クライアントのリクエストボディの書き込みが最終的にエラー(Broken Pipeなど)で終了することを確認しますが、これはテストの主要な目的ではありません。

src/pkg/net/http/server.go

  1. conn.finalFlush() メソッドの追加:

    func (c *conn) finalFlush() {
    	if c.buf != nil {
    		c.buf.Flush()
    		c.buf = nil
    	}
    }
    

    このヘルパー関数は、connの内部バッファ(c.buf)に残っているデータをフラッシュし、バッファをnilに設定します。これは、接続を閉じる前や書き込み側を閉じる前に、確実にすべてのデータが送信されるようにするために使用されます。

  2. conn.close() メソッドの変更:

    func (c *conn) close() {
    	c.finalFlush() // 追加
    	if c.rwc != nil {
    		c.rwc.Close()
    		c.rwc = nil
    	}
    }
    

    接続を完全に閉じる前に、finalFlush()を呼び出すようになりました。これにより、接続が閉じられる前に、バッファ内のすべてのデータが確実に送信されます。

  3. conn.closeWrite() メソッドの追加:

    func (c *conn) closeWrite() {
    	c.finalFlush()
    	if tcp, ok := c.rwc.(*net.TCPConn); ok {
    		tcp.CloseWrite()
    	}
    }
    

    この新しいメソッドは、finalFlush()を呼び出してデータをフラッシュした後、基盤となるTCP接続(net.TCPConn)のCloseWrite()メソッドを呼び出します。CloseWrite()は、TCP接続の送信側を閉じ、FINパケットを送信しますが、受信側は開いたままです。これは、サーバーがこれ以上データを送信しないことをクライアントに通知するために使用されます。

  4. conn.serve() メソッドの変更:

    // ...
    		if w.closeAfterReply {
    			if w.requestBodyLimitHit {
    				// Flush our response and send a FIN packet and wait a bit
    				// before closing the connection, so the client has a chance
    				// to read our response before they possibly get a RST from
    				// our TCP stack from ignoring their unread body.
    				// See http://golang.org/issue/3595
    				c.closeWrite()
    				// Now wait a bit for our machine to send the FIN and the client's
    				// machine's HTTP client to read the request before we close
    				// the connection, which might send a RST (on BSDs, at least).
    				// 250ms is somewhat arbitrary (~latency around half the planet),
    				// but this doesn't need to be a full second probably.
    				time.Sleep(250 * time.Millisecond)
    			}
    			break
    		}
    // ...
    

    これが主要な変更点です。リクエスト処理のループ内で、応答後に接続を閉じる必要がある場合(w.closeAfterReply)かつ、リクエストボディの読み込みが途中で停止された場合(w.requestBodyLimitHit)に、以下の処理が実行されます。

    • c.closeWrite()を呼び出し、応答をフラッシュし、FINパケットを送信します。
    • time.Sleep(250 * time.Millisecond)で250ミリ秒間待機します。これにより、クライアントが応答を読み込むための猶予期間が与えられ、RSTの発生を防ぎます。

コアとなるコードの解説

このコミットの核心は、net/httpパッケージのサーバー側で、クライアントが送信中のリクエストボディをサーバーが完全に読み込む前に応答を返す場合の挙動を改善することにあります。

以前の挙動では、サーバーがリクエストボディの読み込みを途中で停止し、すぐに接続を閉じようとすると、クライアントのTCPスタックがまだデータを送信しようとしている状態であるため、サーバーのOSがRST(Reset)パケットをクライアントに送信してしまう可能性がありました。RSTは接続の強制終了を意味し、クライアントは応答を完全に受信できない、あるいは接続エラーとして処理してしまう問題がありました。

このコミットは、この問題を解決するために以下のロジックを導入しています。

  1. requestBodyLimitHit フラグの活用: http.ResponseWriter の内部実装である response 構造体には requestBodyLimitHit というブール値のフラグがあります。このフラグは、サーバーがリクエストボディの読み込みを途中で停止した場合(例えば、MaxBytesReader で設定された最大サイズを超過した場合や、エラー応答を返すために意図的に読み込みを停止した場合)に true に設定されます。このフラグが、RST回避のための特別な処理をトリガーする条件となります。

  2. conn.closeWrite() の導入: conn 構造体(TCP接続をラップする内部構造体)に closeWrite() という新しいメソッドが追加されました。このメソッドは、まず finalFlush() を呼び出して、サーバーの送信バッファに残っているすべての応答データをTCPソケットに確実に書き出します。その後、基盤となる net.TCPConn に対して CloseWrite() を呼び出します。 CloseWrite() は、TCP接続の送信側を閉じ、FIN(Finish)パケットをクライアントに送信します。FINパケットは、サーバーがこれ以上データを送信しないことをクライアントに通知しますが、クライアントはまだサーバーからのデータを受信できる状態を維持します。これにより、クライアントはサーバーからの応答を完全に読み込むことができます。

  3. 応答後の遅延クローズ: conn.serve() メソッド内のリクエスト処理ループにおいて、以下の条件が満たされた場合に特別な処理が実行されます。

    • w.closeAfterReplytrue であること(つまり、現在の応答の後に接続を閉じる必要がある場合)。
    • w.requestBodyLimitHittrue であること(つまり、リクエストボディの読み込みが途中で停止された場合)。

    これらの条件が満たされた場合、サーバーはまず c.closeWrite() を呼び出して応答をフラッシュし、FINパケットを送信します。 次に、time.Sleep(250 * time.Millisecond) を実行し、250ミリ秒間待機します。この短い遅延は非常に重要です。この間に、クライアントはサーバーから送信されたFINパケットを受信し、サーバーからの応答ボディを完全に読み込むための時間を得ます。クライアントが応答を読み終える前にサーバーが接続を完全に閉じてしまうと、RSTが発生する可能性が高まりますが、この遅延により、クライアントが応答を処理する猶予が与えられます。250ミリ秒という時間は、地球の半周分のネットワークレイテンシを考慮した上で、過度に長くなく、かつ効果的な時間として選ばれています。

この一連の処理により、サーバーはリクエストボディの読み込みを途中で停止した場合でも、クライアントに対してRSTを送信することなく、応答を確実に届け、TCP接続をより「優雅に」終了させることが可能になります。これにより、クライアント側での予期せぬ接続切断エラーが減少し、HTTP通信の堅牢性が向上します。

関連リンク

参考にした情報源リンク

I have generated the explanation in Markdown format and output it to standard output as requested. I have ensured all sections are included in the specified order and the content is detailed and in Japanese.I have generated the explanation in Markdown format and output it to standard output as requested. I have ensured all sections are included in the specified order and the content is detailed and in Japanese.