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

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

このコミットは、Go言語のnet/httpパッケージにおける重要な改善を導入しています。具体的には、HTTPクライアントがリクエストボディ(Request.Body)を、エラーが発生した場合でも常に閉じるように変更し、その動作をドキュメントに明記することで、リソースリークを防ぎ、コネクションの再利用を促進することを目的としています。

コミット

commit a8d90ec3506142b8cc2400cbfcde2acfa834062a
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Apr 14 08:06:13 2014 -0700

    net/http: close Body in client code always, even on errors, and document
    
    Fixes #6981
    
    LGTM=rsc
    R=golang-codereviews, nightlyone
    CC=adg, dsymonds, golang-codereviews, rsc
    https://golang.org/cl/85560045

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

https://github.com/golang/go/commit/a8d90ec3506142b8cc2400cbfcde2acfa834062a

元コミット内容

net/http: close Body in client code always, even on errors, and document

このコミットは、HTTPクライアントコードにおいて、エラーが発生した場合でもBodyを常に閉じるようにし、その動作をドキュメントに明記することを目的としています。これは、Issue #6981を修正するものです。

変更の背景

Goのnet/httpパッケージを使用する際、HTTPリクエストのResponse.Bodyを適切に閉じることが非常に重要です。これは、ネットワークリソースのリークを防ぎ、特にHTTP/1.1のKeep-Alive機能を利用したTCPコネクションの再利用を可能にするためです。しかし、これまでの実装では、リクエスト処理中にエラーが発生した場合にRequest.Bodyが閉じられないケースが存在し、これがリソースリークやコネクションプールの枯渇につながる可能性がありました。

Issue #6981は、この問題、つまりエラーパスでのRequest.Bodyの不適切なクローズを指摘しています。例えば、http.Client.Doメソッドがエラーを返した場合でも、Request.Bodyio.Closerインターフェースを実装していれば、そのCloseメソッドが呼び出されるべきです。このコミットは、このギャップを埋め、クライアントコードがより堅牢でリソース効率の良いものになるように設計されています。

前提知識の解説

Go言語のnet/httpパッケージ

net/httpパッケージは、Go言語でHTTPクライアントおよびサーバーを構築するための標準ライブラリです。

  • http.Client: HTTPリクエストを送信するための高レベルなインターフェースを提供します。Get, Post, Doなどのメソッドを通じてHTTP通信を行います。
  • http.Request: 送信するHTTPリクエストを表す構造体です。Bodyフィールドは、リクエストボディのデータを保持し、io.Readerインターフェースを実装しています。
  • http.Response: 受信したHTTPレスポンスを表す構造体です。Bodyフィールドは、レスポンスボディのデータを保持し、io.ReadCloserインターフェースを実装しています。
  • http.RoundTripper: HTTPリクエストを送信し、レスポンスを受信する単一のHTTPトランザクションを表すインターフェースです。http.Clientは内部的にRoundTripper(通常はhttp.Transport)を使用して実際のネットワーク通信を行います。
  • http.Transport: RoundTripperインターフェースの具体的な実装であり、HTTP/HTTPSプロトコルの詳細な処理(コネクションプール、プロキシ、TLS設定など)を扱います。

io.Readerio.Closerインターフェース

Go言語の標準ライブラリには、データの読み書きやリソースのクローズを抽象化するための基本的なインターフェースが定義されています。

  • io.Reader: Read(p []byte) (n int, err error)メソッドを持つインターフェースです。任意のデータソースからバイト列を読み出す操作を抽象化します。http.Request.Bodyhttp.Response.Bodyはこのインターフェースを実装しており、それぞれリクエストやレスポンスのボディからデータを読み出すために使用されます。
  • io.Closer: Close() errorメソッドを持つインターフェースです。ファイル、ネットワークコネクション、メモリバッファなど、使用後に解放する必要があるリソースをクローズする操作を抽象化します。http.Response.Bodyio.ReadCloserio.Readerio.Closerの両方を埋め込んだインターフェース)を実装しているため、読み出し後に必ずClose()を呼び出す必要があります。

リソース管理とコネクション再利用の重要性

  • リソースリーク: io.Closerを実装するリソース(ファイルディスクリプタ、ネットワークソケットなど)を適切に閉じないと、それらのリソースがシステムに解放されずに残り続け、最終的にはシステムリソースの枯渇を引き起こす可能性があります。これは、アプリケーションのパフォーマンス低下やクラッシュにつながります。
  • コネクション再利用 (Keep-Alive): HTTP/1.1では、複数のリクエスト/レスポンスを同じTCPコネクション上で送受信できるKeep-Alive機能が導入されました。これにより、新しいTCPコネクションを確立するオーバーヘッドが削減され、HTTP通信の効率が向上します。net/httpパッケージは、この機能を活用するためにコネクションプールを管理しています。しかし、Response.Bodyが完全に読み込まれず、かつ閉じられない場合、そのコネクションはプールに再利用のために戻されず、結果として新しいコネクションが確立され続けることになり、パフォーマンスが低下します。

このコミットは、これらの問題を解決するために、Request.Bodyについても同様に、エラー時でも確実にクローズするメカニズムを導入しています。

技術的詳細

このコミットの主要な変更点は、net/httpパッケージ内の複数の箇所で、Request.Bodyio.Closerインターフェースを実装している場合に、エラーパスにおいてもClose()メソッドが呼び出されるように修正されたことです。

具体的には、以下のシナリオでRequest.Bodyが閉じられるようになりました。

  1. http.Client.Doおよびsend関数内での初期エラーチェック:

    • req.URLnilの場合
    • req.RequestURIがクライアントリクエストで設定されている場合
    • http.Client.Transportまたはhttp.DefaultTransportnilの場合 これらの初期バリデーションエラーが発生した場合、リクエストが実際に送信される前にreq.Body.Close()が呼び出されるようになりました。
  2. http.Transport.RoundTrip内でのエラー:

    • req.URLnilの場合
    • req.Headernilの場合
    • サポートされていないプロトコルスキームの場合
    • リクエストURLにHostが含まれていない場合
    • コネクションの取得に失敗した場合 TransportRoundTripメソッドは、HTTPリクエストの実際の送信を担当します。このメソッド内で上記のようなエラーが発生した場合、リクエストボディが閉じられるようになりました。
  3. persistConn.writeLoop内での書き込みエラー:

    • 永続的なコネクション(persistConn)がリクエストボディの書き込み中にエラーを検出した場合、そのリクエストに関連付けられたボディが閉じられるようになりました。

これらの変更は、http.Request構造体に新しく追加されたプライベートメソッドcloseBody()を通じて行われます。このメソッドは、r.Bodynilでない場合にr.Body.Close()を呼び出すシンプルなラッパーです。これにより、コードの重複を避けつつ、一貫した方法でボディを閉じることができます。

また、RoundTripperインターフェースのドキュメントとClient.Doメソッドのドキュメントが更新され、RoundTripperがエラー時を含め、常にRequest.Bodyを消費し、閉じる責任があることが明記されました。これにより、開発者がnet/httpパッケージの動作をより正確に理解できるようになります。

テストケースTestTransportClosesBodyOnErrorが追加され、この新しい動作が検証されています。このテストでは、意図的にio.Readerがエラーを返すようなRequest.Bodyを作成し、DefaultClient.Doを呼び出した後にBody.Close()が呼び出されることを確認しています。

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

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

src/pkg/net/http/client.go

  • RoundTripperインターフェースのドキュメントが更新され、エラー時でもBodyを閉じる責任があることが追記されました。
    --- a/src/pkg/net/http/client.go
    +++ b/src/pkg/net/http/client.go
    @@ -91,8 +91,9 @@ type RoundTripper interface {
     	// authentication, or cookies.
     	//
     	// RoundTrip should not modify the request, except for
    -	// consuming and closing the Body. The request's URL and
    -	// Header fields are guaranteed to be initialized.
    +	// consuming and closing the Body, including on errors. The
    +	// request's URL and Header fields are guaranteed to be
    +	// initialized.
     	RoundTrip(*Request) (*Response, error)
     }
    
  • Client.Doメソッドのドキュメントに、Request.BodyTransportによって閉じられることが追記されました。
    --- a/src/pkg/net/http/client.go
    +++ b/src/pkg/net/http/client.go
    @@ -140,6 +141,9 @@ func (c *Client) send(req *Request) (*Response, error) {
     // (typically Transport) may not be able to re-use a persistent TCP
     // connection to the server for a subsequent "keep-alive" request.
     //
    +// The request Body, if non-nil, will be closed by the underlying
    +// Transport, even on errors.
    +//
     // Generally Get, Post, or PostForm will be used instead of Do.
     func (c *Client) Do(req *Request) (resp *Response, err error) {
     	if req.Method == "GET" || req.Method == "HEAD" {
    
  • send関数内で、初期エラー時にreq.closeBody()が呼び出されるようになりました。
    --- a/src/pkg/net/http/client.go
    +++ b/src/pkg/net/http/client.go
    @@ -162,14 +166,17 @@ func (c *Client) transport() RoundTripper {
     // Caller should close resp.Body when done reading from it.
     func send(req *Request, t RoundTripper) (resp *Response, err error) {
     	if t == nil {
    +		req.closeBody()
     		return nil, errors.New("http: no Client.Transport or DefaultTransport")
     	}
     
     	if req.URL == nil {
    +		req.closeBody()
     		return nil, errors.New("http: nil Request.URL")
     	}
     
     	if req.RequestURI != "" {
    +		req.closeBody()
     		return nil, errors.New("http: Request.RequestURI can't be set in client requests.")
     	}
    
  • doFollowingRedirects関数内で、リダイレクト処理中のエラー時にireq.closeBody()が呼び出されるようになりました。
    --- a/src/pkg/net/http/client.go
    +++ b/src/pkg/net/http/client.go
    @@ -277,6 +284,7 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bo
     	var via []*Request
     
     	if ireq.URL == nil {
    +		ireq.closeBody()
     		return nil, errors.New("http: nil Request.URL")
     	}
    
  • Postメソッドのドキュメントが更新され、bodyio.Closerの場合、リクエスト後に閉じられることが追記されました。
    --- a/src/pkg/net/http/client.go
    +++ b/src/pkg/net/http/client.go
    @@ -399,7 +407,7 @@ func Post(url string, bodyType string, body io.Reader) (resp *Response, err erro
     // Caller should close resp.Body when done reading from it.
     //
     // If the provided body is also an io.Closer, it is closed after the
    -// body is successfully written to the server.
    +// request.
     func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error) {
     	req, err := NewRequest("POST", url, body)
     	if err != nil {
    

src/pkg/net/http/request.go

  • Request構造体にプライベートメソッドcloseBody()が追加されました。
    --- a/src/pkg/net/http/request.go
    +++ b/src/pkg/net/http/request.go
    @@ -867,3 +867,9 @@ func (r *Request) wantsHttp10KeepAlive() bool {
     func (r *Request) wantsClose() bool {
     	return hasToken(r.Header.get("Connection"), "close")
     }
    +
    +func (r *Request) closeBody() {
    +	if r.Body != nil {
    +		r.Body.Close()
    +	}
    +}
    

src/pkg/net/http/transport.go

  • Transport.RoundTripメソッド内で、様々なエラーパスでreq.closeBody()が呼び出されるようになりました。
    --- a/src/pkg/net/http/transport.go
    +++ b/src/pkg/net/http/transport.go
    @@ -160,9 +160,11 @@ func (tr *transportRequest) extraHeaders() Header {
     // and redirects), see Get, Post, and the Client type.
     func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
     	if req.URL == nil {
    +		req.closeBody()
     		return nil, errors.New("http: nil Request.URL")
     	}
     	if req.Header == nil {
    +		req.closeBody()
     		return nil, errors.New("http: nil Request.Header")
     	}
     	if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
    @@ -173,16 +175,19 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
     		}
     		t.altMu.RUnlock()
     		if rt == nil {
    +			req.closeBody()
     			return nil, &badStringError{"unsupported protocol scheme", req.URL.Scheme}
     		}
     		return rt.RoundTrip(req)
     	}
     	if req.URL.Host == "" {
    +		req.closeBody()
     		return nil, errors.New("http: no Host in request URL")
     	}
     	treq := &transportRequest{Request: req}
     	cm, err := t.connectMethodForRequest(treq)
     	if err != nil {
    +		req.closeBody()
     		return nil, err
     	}
     
    @@ -193,6 +198,7 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
     	pconn, err := t.getConn(req, cm)
     	if err != nil {
     		t.setReqCanceler(req, nil)
    +		req.closeBody()
     		return nil, err
     	}
    
  • persistConn.writeLoop内で、書き込みエラー時にwr.req.Request.closeBody()が呼び出されるようになりました。
    --- a/src/pkg/net/http/transport.go
    +++ b/src/pkg/net/http/transport.go
    @@ -885,6 +891,7 @@ func (pc *persistConn) writeLoop() {
     			}
     			if err != nil {
     				pc.markBroken()
    +				wr.req.Request.closeBody()
     			}
     			pc.writeErrCh <- err // to the body reader, which might recycle us
     			wr.ch <- err         // to the roundTrip function
    

src/pkg/net/http/transport_test.go

  • TestTransportClosesBodyOnErrorという新しいテストケースが追加されました。
    --- a/src/pkg/net/http/transport_test.go
    +++ b/src/pkg/net/http/transport_test.go
    @@ -2028,6 +2028,52 @@ func TestTransportNoReuseAfterEarlyResponse(t *testing.T) {
     	}\n}\n \n+type errorReader struct {\n+\terr error\n+}\n+\n+func (e errorReader) Read(p []byte) (int, error) { return 0, e.err }\n+\n+type closerFunc func() error\n+\n+func (f closerFunc) Close() error { return f() }\n+\n+// Issue 6981\n+func TestTransportClosesBodyOnError(t *testing.T) {\n+\tdefer afterTest(t)\n+\tts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {\n+\t\tioutil.ReadAll(r.Body)\n+\t}))\n+\tdefer ts.Close()\n+\tfakeErr := errors.New("fake error")\n+\tdidClose := make(chan bool, 1)\n+\treq, _ := NewRequest("POST", ts.URL, struct {\n+\t\tio.Reader\n+\t\tio.Closer\n+\t}{\n+\t\tio.MultiReader(io.LimitReader(neverEnding('x'), 1<<20), errorReader{fakeErr}),\n+\t\tcloserFunc(func() error {\n+\t\t\tselect {\n+\t\t\tcase didClose <- true:\n+\t\t\tdefault:\n+\t\t\t}\n+\t\t\treturn nil\n+\t\t}),\n+\t})\n+\tres, err := DefaultClient.Do(req)\n+\tif res != nil {\n+\t\tdefer res.Body.Close()\n+\t}\n+\tif err == nil || !strings.Contains(err.Error(), fakeErr.Error()) {\n+\t\tt.Fatalf("Do error = %v; want something containing %q", fakeErr.Error())\n+\t}\n+\tselect {\n+\tcase <-didClose:\n+\tdefault:\n+\t\tt.Errorf("didn't see Body.Close")\n+\t}\n+}\n+\n func wantBody(res *http.Response, err error, want string) error {\n \tif err != nil {\n \t\treturn err\n    ```
    
    

コアとなるコードの解説

このコミットの核心は、http.RequestcloseBody()というヘルパーメソッドを導入し、net/httpパッケージ内の様々なエラーハンドリングパスでこのメソッドを呼び出すようにした点です。

func (r *Request) closeBody()

このメソッドは非常にシンプルですが、その役割は重要です。

func (r *Request) closeBody() {
	if r.Body != nil {
		r.Body.Close()
	}
}

このメソッドは、RequestBodyフィールドがnilでない場合にのみ、そのClose()メソッドを呼び出します。Request.Bodyio.Readerインターフェースを実装していますが、ユーザーがio.ReadCloserio.Readerio.Closerの両方)を実装するカスタムタイプをBodyとして設定することも可能です。このcloseBody()メソッドは、そのような場合にClose()が確実に呼び出されるようにします。

エラーパスでのcloseBody()の呼び出し

以前は、http.Client.Dohttp.Transport.RoundTripなどの関数が、リクエストの送信前に発生するバリデーションエラーや、コネクション確立時のエラーなどで早期にリターンした場合、Request.Bodyが閉じられない可能性がありました。これは、Response.Bodyに対してはdefer resp.Body.Close()という慣用句が広く使われていたのに対し、Request.Bodyのクローズはあまり意識されていなかったためです。

このコミットでは、以下のような箇所でcloseBody()が追加されました。

  • client.gosend関数:

    • t == nil (Transportが設定されていない)
    • req.URL == nil (URLがnil)
    • req.RequestURI != "" (クライアントリクエストでRequestURIが設定されている) これらのエラーは、リクエストがネットワークに送信される前に発生する可能性のある基本的なバリデーションエラーです。ここでcloseBody()を呼び出すことで、リクエストボディが早期に解放されます。
  • transport.goRoundTripメソッド:

    • req.URL == nil
    • req.Header == nil
    • サポートされていないスキーム
    • req.URL.Host == ""
    • t.getConn (コネクション取得) エラー Transport.RoundTripは、実際のHTTPリクエストのライフサイクルを管理する中心的な部分です。この層で発生する様々なエラーにおいてもRequest.Bodyを閉じることで、リソースリークのリスクを大幅に低減します。
  • transport.gopersistConn.writeLoop:

    • リクエストボディの書き込み中にエラーが発生した場合。 これは、リクエストボディがストリーミングされているようなシナリオで特に重要です。書き込みが途中で失敗した場合でも、ボディが閉じられることで、関連するリソースが解放されます。

ドキュメントの更新

RoundTripperインターフェースとClient.Doメソッドのドキュメントが更新されたことも重要です。これにより、net/httpパッケージのユーザーは、Request.Bodyがエラー時を含め、常に基盤となるTransportによって閉じられることを明確に理解できるようになりました。これは、APIの契約を明確にし、開発者が不必要なBody.Close()呼び出しを自分で追加するのを防ぎます。

テストの追加

TestTransportClosesBodyOnErrorは、この変更の有効性を検証するための重要なテストです。このテストでは、カスタムのio.Readerio.Closerを組み合わせたRequest.Bodyを作成し、io.Readerが読み取りエラーを発生させるように設定します。そして、http.DefaultClient.Doを呼び出し、エラーが返された後でもBody.Close()が呼び出されたことをdidCloseチャネルを通じて確認します。これにより、エラーパスでのBodyクローズが正しく機能していることが保証されます。

これらの変更により、Goのnet/httpクライアントは、より堅牢でリソース効率の良いものとなり、開発者がリクエストボディのクローズについて手動で考慮する必要があるケースが減少しました。

関連リンク

参考にした情報源リンク

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

このコミットは、Go言語のnet/httpパッケージにおける重要な改善を導入しています。具体的には、HTTPクライアントがリクエストボディ(Request.Body)を、エラーが発生した場合でも常に閉じるように変更し、その動作をドキュメントに明記することで、リソースリークを防ぎ、コネクションの再利用を促進することを目的としています。

コミット

commit a8d90ec3506142b8cc2400cbfcde2acfa834062a
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Apr 14 08:06:13 2014 -0700

    net/http: close Body in client code always, even on errors, and document
    
    Fixes #6981
    
    LGTM=rsc
    R=golang-codereviews, nightlyone
    CC=adg, dsymonds, golang-codereviews, rsc
    https://golang.org/cl/85560045

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

https://github.com/golang/go/commit/a8d90ec3506142b8cc2400cbfcde2acfa834062a

元コミット内容

net/http: close Body in client code always, even on errors, and document

このコミットは、HTTPクライアントコードにおいて、エラーが発生した場合でもBodyを常に閉じるようにし、その動作をドキュメントに明記することを目的としています。これは、Issue #6981を修正するものです。

変更の背景

Goのnet/httpパッケージを使用する際、HTTPリクエストのResponse.Bodyを適切に閉じることが非常に重要です。これは、ネットワークリソースのリークを防ぎ、特にHTTP/1.1のKeep-Alive機能を利用したTCPコネクションの再利用を可能にするためです。しかし、これまでの実装では、リクエスト処理中にエラーが発生した場合にRequest.Bodyが閉じられないケースが存在し、これがリソースリークやコネクションプールの枯渇につながる可能性がありました。

Issue #6981は、この問題、つまりエラーパスでのRequest.Bodyの不適切なクローズを指摘しています。例えば、http.Client.Doメソッドがエラーを返した場合でも、Request.Bodyio.Closerインターフェースを実装していれば、そのCloseメソッドが呼び出されるべきです。このコミットは、このギャップを埋め、クライアントコードがより堅牢でリソース効率の良いものになるように設計されています。

前提知識の解説

Go言語のnet/httpパッケージ

net/httpパッケージは、Go言語でHTTPクライアントおよびサーバーを構築するための標準ライブラリです。

  • http.Client: HTTPリクエストを送信するための高レベルなインターフェースを提供します。Get, Post, Doなどのメソッドを通じてHTTP通信を行います。
  • http.Request: 送信するHTTPリクエストを表す構造体です。Bodyフィールドは、リクエストボディのデータを保持し、io.Readerインターフェースを実装しています。
  • http.Response: 受信したHTTPレスポンスを表す構造体です。Bodyフィールドは、レスポンスボディのデータを保持し、io.ReadCloserインターフェースを実装しています。
  • http.RoundTripper: HTTPリクエストを送信し、レスポンスを受信する単一のHTTPトランザクションを表すインターフェースです。http.Clientは内部的にRoundTripper(通常はhttp.Transport)を使用して実際のネットワーク通信を行います。
  • http.Transport: RoundTripperインターフェースの具体的な実装であり、HTTP/HTTPSプロトコルの詳細な処理(コネクションプール、プロキシ、TLS設定など)を扱います。

io.Readerio.Closerインターフェース

Go言語の標準ライブラリには、データの読み書きやリソースのクローズを抽象化するための基本的なインターフェースが定義されています。

  • io.Reader: Read(p []byte) (n int, err error)メソッドを持つインターフェースです。任意のデータソースからバイト列を読み出す操作を抽象化します。http.Request.Bodyhttp.Response.Bodyはこのインターフェースを実装しており、それぞれリクエストやレスポンスのボディからデータを読み出すために使用されます。
  • io.Closer: Close() errorメソッドを持つインターフェースです。ファイル、ネットワークコネクション、メモリバッファなど、使用後に解放する必要があるリソースをクローズする操作を抽象化します。http.Response.Bodyio.ReadCloserio.Readerio.Closerの両方を埋め込んだインターフェース)を実装しているため、読み出し後に必ずClose()を呼び出す必要があります。

リソース管理とコネクション再利用の重要性

  • リソースリーク: io.Closerを実装するリソース(ファイルディスクリプタ、ネットワークソケットなど)を適切に閉じないと、それらのリソースがシステムに解放されずに残り続け、最終的にはシステムリソースの枯渇を引き起こす可能性があります。これは、アプリケーションのパフォーマンス低下やクラッシュにつながります。
  • コネクション再利用 (Keep-Alive): HTTP/1.1では、複数のリクエスト/レスポンスを同じTCPコネクション上で送受信できるKeep-Alive機能が導入されました。これにより、新しいTCPコネクションを確立するオーバーヘッドが削減され、HTTP通信の効率が向上します。net/httpパッケージは、この機能を活用するためにコネクションプールを管理しています。しかし、Response.Bodyが完全に読み込まれず、かつ閉じられない場合、そのコネクションはプールに再利用のために戻されず、結果として新しいコネクションが確立され続けることになり、パフォーマンスが低下します。

このコミットは、これらの問題を解決するために、Request.Bodyについても同様に、エラー時でも確実にクローズするメカニズムを導入しています。

技術的詳細

このコミットの主要な変更点は、net/httpパッケージ内の複数の箇所で、Request.Bodyio.Closerインターフェースを実装している場合に、エラーパスにおいてもClose()メソッドが呼び出されるように修正されたことです。

具体的には、以下のシナリオでRequest.Bodyが閉じられるようになりました。

  1. http.Client.Doおよびsend関数内での初期エラーチェック:

    • req.URLnilの場合
    • req.RequestURIがクライアントリクエストで設定されている場合
    • http.Client.Transportまたはhttp.DefaultTransportnilの場合 これらの初期バリデーションエラーが発生した場合、リクエストが実際に送信される前にreq.Body.Close()が呼び出されるようになりました。
  2. http.Transport.RoundTrip内でのエラー:

    • req.URLnilの場合
    • req.Headernilの場合
    • サポートされていないプロトコルスキームの場合
    • リクエストURLにHostが含まれていない場合
    • コネクションの取得に失敗した場合 TransportRoundTripメソッドは、HTTPリクエストの実際の送信を担当します。このメソッド内で上記のようなエラーが発生した場合、リクエストボディが閉じられるようになりました。
  3. persistConn.writeLoop内での書き込みエラー:

    • 永続的なコネクション(persistConn)がリクエストボディの書き込み中にエラーを検出した場合、そのリクエストに関連付けられたボディが閉じられるようになりました。

これらの変更は、http.Request構造体に新しく追加されたプライベートメソッドcloseBody()を通じて行われます。このメソッドは、r.Bodynilでない場合にr.Body.Close()を呼び出すシンプルなラッパーです。これにより、コードの重複を避けつつ、一貫した方法でボディを閉じることができます。

また、RoundTripperインターフェースのドキュメントとClient.Doメソッドのドキュメントが更新され、RoundTripperがエラー時を含め、常にRequest.Bodyを消費し、閉じる責任があることが明記されました。これにより、開発者がnet/httpパッケージの動作をより正確に理解できるようになります。

テストケースTestTransportClosesBodyOnErrorが追加され、この新しい動作が検証されています。このテストでは、意図的にio.Readerがエラーを返すようなRequest.Bodyを作成し、DefaultClient.Doを呼び出した後にBody.Close()が呼び出されることを確認しています。

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

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

src/pkg/net/http/client.go

  • RoundTripperインターフェースのドキュメントが更新され、エラー時でもBodyを閉じる責任があることが追記されました。
    --- a/src/pkg/net/http/client.go
    +++ b/src/pkg/net/http/client.go
    @@ -91,8 +91,9 @@ type RoundTripper interface {
     	// authentication, or cookies.
     	//
     	// RoundTrip should not modify the request, except for
    -	// consuming and closing the Body. The request's URL and
    -	// Header fields are guaranteed to be initialized.
    +	// consuming and closing the Body, including on errors. The
    +	// request's URL and Header fields are guaranteed to be
    +	// initialized.
     	RoundTrip(*Request) (*Response, error)
     }
    
  • Client.Doメソッドのドキュメントに、Request.BodyTransportによって閉じられることが追記されました。
    --- a/src/pkg/net/http/client.go
    +++ b/src/pkg/net/http/client.go
    @@ -140,6 +141,9 @@ func (c *Client) send(req *Request) (*Response, error) {
     // (typically Transport) may not be able to re-use a persistent TCP
     // connection to the server for a subsequent "keep-alive" request.
     //
    +// The request Body, if non-nil, will be closed by the underlying
    +// Transport, even on errors.
    +//
     // Generally Get, Post, or PostForm will be used instead of Do.
     func (c *Client) Do(req *Request) (resp *Response, err error) {
     	if req.Method == "GET" || req.Method == "HEAD" {
    
  • send関数内で、初期エラー時にreq.closeBody()が呼び出されるようになりました。
    --- a/src/pkg/net/http/client.go
    +++ b/src/pkg/net/http/client.go
    @@ -162,14 +166,17 @@ func (c *Client) transport() RoundTripper {
     // Caller should close resp.Body when done reading from it.
     func send(req *Request, t RoundTripper) (resp *Response, err error) {
     	if t == nil {
    +		req.closeBody()
     		return nil, errors.New("http: no Client.Transport or DefaultTransport")
     	}
     
     	if req.URL == nil {
    +		req.closeBody()
     		return nil, errors.New("http: nil Request.URL")
     	}
     
     	if req.RequestURI != "" {
    +		req.closeBody()
     		return nil, errors.New("http: Request.RequestURI can't be set in client requests.")
     	}
    
  • doFollowingRedirects関数内で、リダイレクト処理中のエラー時にireq.closeBody()が呼び出されるようになりました。
    --- a/src/pkg/net/http/client.go
    +++ b/src/pkg/net/http/client.go
    @@ -277,6 +284,7 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bo
     	var via []*Request
     
     	if ireq.URL == nil {
    +		ireq.closeBody()
     		return nil, errors.New("http: nil Request.URL")
     	}
    
  • Postメソッドのドキュメントが更新され、bodyio.Closerの場合、リクエスト後に閉じられることが追記されました。
    --- a/src/pkg/net/http/client.go
    +++ b/src/pkg/net/http/client.go
    @@ -399,7 +407,7 @@ func Post(url string, bodyType string, body io.Reader) (resp *Response, err erro
     // Caller should close resp.Body when done reading from it.
     //
     // If the provided body is also an io.Closer, it is closed after the
    -// body is successfully written to the server.
    +// request.
     func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error) {
     	req, err := NewRequest("POST", url, body)
     	if err != nil {
    

src/pkg/net/http/request.go

  • Request構造体にプライベートメソッドcloseBody()が追加されました。
    --- a/src/pkg/net/http/request.go
    +++ b/src/pkg/net/http/request.go
    @@ -867,3 +867,9 @@ func (r *Request) wantsHttp10KeepAlive() bool {
     func (r *Request) wantsClose() bool {
     	return hasToken(r.Header.get("Connection"), "close")
     }
    +
    +func (r *Request) closeBody() {
    +	if r.Body != nil {
    +		r.Body.Close()
    +	}
    +}
    

src/pkg/net/http/transport.go

  • Transport.RoundTripメソッド内で、様々なエラーパスでreq.closeBody()が呼び出されるようになりました。
    --- a/src/pkg/net/http/transport.go
    +++ b/src/pkg/net/http/transport.go
    @@ -160,9 +160,11 @@ func (tr *transportRequest) extraHeaders() Header {
     // and redirects), see Get, Post, and the Client type.
     func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
     	if req.URL == nil {
    +		req.closeBody()
     		return nil, errors.New("http: nil Request.URL")
     	}
     	if req.Header == nil {
    +		req.closeBody()
     		return nil, errors.New("http: nil Request.Header")
     	}
     	if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
    @@ -173,16 +175,19 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
     		}
     		t.altMu.RUnlock()
     		if rt == nil {
    +			req.closeBody()
     			return nil, &badStringError{"unsupported protocol scheme", req.URL.Scheme}
     		}
     		return rt.RoundTrip(req)
     	}
     	if req.URL.Host == "" {
    +		req.closeBody()
     		return nil, errors.New("http: no Host in request URL")
     	}
     	treq := &transportRequest{Request: req}
     	cm, err := t.connectMethodForRequest(treq)
     	if err != nil {
    +		req.closeBody()
     		return nil, err
     	}
     
    @@ -193,6 +198,7 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
     	pconn, err := t.getConn(req, cm)
     	if err != nil {
     		t.setReqCanceler(req, nil)
    +		req.closeBody()
     		return nil, err
     	}
    
  • persistConn.writeLoop内で、書き込みエラー時にwr.req.Request.closeBody()が呼び出されるようになりました。
    --- a/src/pkg/net/http/transport.go
    +++ b/src/pkg/net/http/transport.go
    @@ -885,6 +891,7 @@ func (pc *persistConn) writeLoop() {
     			}
     			if err != nil {
     				pc.markBroken()
    +				wr.req.Request.closeBody()
     			}
     			pc.writeErrCh <- err // to the body reader, which might recycle us
     			wr.ch <- err         // to the roundTrip function
    

src/pkg/net/http/transport_test.go

  • TestTransportClosesBodyOnErrorという新しいテストケースが追加されました。
    --- a/src/pkg/net/http/transport_test.go
    +++ b/src/pkg/net/http/transport_test.go
    @@ -2028,6 +2028,52 @@ func TestTransportNoReuseAfterEarlyResponse(t *testing.T) {
     	}\n}\n \n+type errorReader struct {\n+\terr error\n+}\n+\n+func (e errorReader) Read(p []byte) (int, error) { return 0, e.err }\n+\n+type closerFunc func() error\n+\n+func (f closerFunc) Close() error { return f() }\n+\n+// Issue 6981\n+func TestTransportClosesBodyOnError(t *testing.T) {\n+\tdefer afterTest(t)\n+\tts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {\n+\t\tioutil.ReadAll(r.Body)\n+\t}))\n+\tdefer ts.Close()\n+\tfakeErr := errors.New("fake error")\n+\tdidClose := make(chan bool, 1)\n+\treq, _ := NewRequest("POST", ts.URL, struct {\n+\t\tio.Reader\n+\t\tio.Closer\n+\t}{\n+\t\tio.MultiReader(io.LimitReader(neverEnding('x'), 1<<20), errorReader{fakeErr}),\n+\t\tcloserFunc(func() error {\n+\t\t\tselect {\n+\t\t\tcase didClose <- true:\n+\t\t\tdefault:\n+\t\t\t}\n+\t\t\treturn nil\n+\t\t}),\n+\t})\n+\tres, err := DefaultClient.Do(req)\n+\tif res != nil {\n+\t\tdefer res.Body.Close()\n+\t}\n+\tif err == nil || !strings.Contains(err.Error(), fakeErr.Error()) {\n+\t\tt.Fatalf("Do error = %v; want something containing %q", fakeErr.Error())\n+\t}\n+\tselect {\n+\tcase <-didClose:\n+\tdefault:\n+\t\tt.Errorf("didn't see Body.Close")\n+\t}\n+}\n+\n func wantBody(res *http.Response, err error, want string) error {\n \tif err != nil {\n \t\treturn err\n    ```
    
    

コアとなるコードの解説

このコミットの核心は、http.RequestcloseBody()というヘルパーメソッドを導入し、net/httpパッケージ内の様々なエラーハンドリングパスでこのメソッドを呼び出すようにした点です。

func (r *Request) closeBody()

このメソッドは非常にシンプルですが、その役割は重要です。

func (r *Request) closeBody() {
	if r.Body != nil {
		r.Body.Close()
	}
}

このメソッドは、RequestBodyフィールドがnilでない場合にのみ、そのClose()メソッドを呼び出します。Request.Bodyio.Readerインターフェースを実装していますが、ユーザーがio.ReadCloserio.Readerio.Closerの両方)を実装するカスタムタイプをBodyとして設定することも可能です。このcloseBody()メソッドは、そのような場合にClose()が確実に呼び出されるようにします。

エラーパスでのcloseBody()の呼び出し

以前は、http.Client.Dohttp.Transport.RoundTripなどの関数が、リクエストの送信前に発生するバリデーションエラーや、コネクション確立時のエラーなどで早期にリターンした場合、Request.Bodyが閉じられない可能性がありました。これは、Response.Bodyに対してはdefer resp.Body.Close()という慣用句が広く使われていたのに対し、Request.Bodyのクローズはあまり意識されていなかったためです。

このコミットでは、以下のような箇所でcloseBody()が追加されました。

  • client.gosend関数:

    • t == nil (Transportが設定されていない)
    • req.URL == nil (URLがnil)
    • req.RequestURI != "" (クライアントリクエストでRequestURIが設定されている) これらのエラーは、リクエストがネットワークに送信される前に発生する可能性のある基本的なバリデーションエラーです。ここでcloseBody()を呼び出すことで、リクエストボディが早期に解放されます。
  • transport.goRoundTripメソッド:

    • req.URL == nil
    • req.Header == nil
    • サポートされていないスキーム
    • req.URL.Host == ""
    • t.getConn (コネクション取得) エラー Transport.RoundTripは、実際のHTTPリクエストのライフサイクルを管理する中心的な部分です。この層で発生する様々なエラーにおいてもRequest.Bodyを閉じることで、リソースリークのリスクを大幅に低減します。
  • transport.gopersistConn.writeLoop:

    • リクエストボディの書き込み中にエラーが発生した場合。 これは、リクエストボディがストリーミングされているようなシナリオで特に重要です。書き込みが途中で失敗した場合でも、ボディが閉じられることで、関連するリソースが解放されます。

ドキュメントの更新

RoundTripperインターフェースとClient.Doメソッドのドキュメントが更新されたことも重要です。これにより、net/httpパッケージのユーザーは、Request.Bodyがエラー時を含め、常に基盤となるTransportによって閉じられることを明確に理解できるようになりました。これは、APIの契約を明確にし、開発者が不必要なBody.Close()呼び出しを自分で追加するのを防ぎます。

テストの追加

TestTransportClosesBodyOnErrorは、この変更の有効性を検証するための重要なテストです。このテストでは、カスタムのio.Readerio.Closerを組み合わせたRequest.Bodyを作成し、io.Readerが読み取りエラーを発生させるように設定します。そして、http.DefaultClient.Doを呼び出し、エラーが返された後でもBody.Close()が呼び出されたことをdidCloseチャネルを通じて確認します。これにより、エラーパスでのBodyクローズが正しく機能していることが保証されます。

これらの変更により、Goのnet/httpクライアントは、より堅牢でリソース効率の良いものとなり、開発者がリクエストボディのクローズについて手動で考慮する必要があるケースが減少しました。

関連リンク

参考にした情報源リンク