[インデックス 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.Body
がio.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.Reader
とio.Closer
インターフェース
Go言語の標準ライブラリには、データの読み書きやリソースのクローズを抽象化するための基本的なインターフェースが定義されています。
io.Reader
:Read(p []byte) (n int, err error)
メソッドを持つインターフェースです。任意のデータソースからバイト列を読み出す操作を抽象化します。http.Request.Body
やhttp.Response.Body
はこのインターフェースを実装しており、それぞれリクエストやレスポンスのボディからデータを読み出すために使用されます。io.Closer
:Close() error
メソッドを持つインターフェースです。ファイル、ネットワークコネクション、メモリバッファなど、使用後に解放する必要があるリソースをクローズする操作を抽象化します。http.Response.Body
はio.ReadCloser
(io.Reader
とio.Closer
の両方を埋め込んだインターフェース)を実装しているため、読み出し後に必ずClose()
を呼び出す必要があります。
リソース管理とコネクション再利用の重要性
- リソースリーク:
io.Closer
を実装するリソース(ファイルディスクリプタ、ネットワークソケットなど)を適切に閉じないと、それらのリソースがシステムに解放されずに残り続け、最終的にはシステムリソースの枯渇を引き起こす可能性があります。これは、アプリケーションのパフォーマンス低下やクラッシュにつながります。 - コネクション再利用 (Keep-Alive): HTTP/1.1では、複数のリクエスト/レスポンスを同じTCPコネクション上で送受信できるKeep-Alive機能が導入されました。これにより、新しいTCPコネクションを確立するオーバーヘッドが削減され、HTTP通信の効率が向上します。
net/http
パッケージは、この機能を活用するためにコネクションプールを管理しています。しかし、Response.Body
が完全に読み込まれず、かつ閉じられない場合、そのコネクションはプールに再利用のために戻されず、結果として新しいコネクションが確立され続けることになり、パフォーマンスが低下します。
このコミットは、これらの問題を解決するために、Request.Body
についても同様に、エラー時でも確実にクローズするメカニズムを導入しています。
技術的詳細
このコミットの主要な変更点は、net/http
パッケージ内の複数の箇所で、Request.Body
がio.Closer
インターフェースを実装している場合に、エラーパスにおいてもClose()
メソッドが呼び出されるように修正されたことです。
具体的には、以下のシナリオでRequest.Body
が閉じられるようになりました。
-
http.Client.Do
およびsend
関数内での初期エラーチェック:req.URL
がnil
の場合req.RequestURI
がクライアントリクエストで設定されている場合http.Client.Transport
またはhttp.DefaultTransport
がnil
の場合 これらの初期バリデーションエラーが発生した場合、リクエストが実際に送信される前にreq.Body.Close()
が呼び出されるようになりました。
-
http.Transport.RoundTrip
内でのエラー:req.URL
がnil
の場合req.Header
がnil
の場合- サポートされていないプロトコルスキームの場合
- リクエストURLに
Host
が含まれていない場合 - コネクションの取得に失敗した場合
Transport
のRoundTrip
メソッドは、HTTPリクエストの実際の送信を担当します。このメソッド内で上記のようなエラーが発生した場合、リクエストボディが閉じられるようになりました。
-
persistConn.writeLoop
内での書き込みエラー:- 永続的なコネクション(
persistConn
)がリクエストボディの書き込み中にエラーを検出した場合、そのリクエストに関連付けられたボディが閉じられるようになりました。
- 永続的なコネクション(
これらの変更は、http.Request
構造体に新しく追加されたプライベートメソッドcloseBody()
を通じて行われます。このメソッドは、r.Body
がnil
でない場合に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.Body
がTransport
によって閉じられることが追記されました。--- 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
メソッドのドキュメントが更新され、body
がio.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.Request
にcloseBody()
というヘルパーメソッドを導入し、net/http
パッケージ内の様々なエラーハンドリングパスでこのメソッドを呼び出すようにした点です。
func (r *Request) closeBody()
このメソッドは非常にシンプルですが、その役割は重要です。
func (r *Request) closeBody() {
if r.Body != nil {
r.Body.Close()
}
}
このメソッドは、Request
のBody
フィールドがnil
でない場合にのみ、そのClose()
メソッドを呼び出します。Request.Body
はio.Reader
インターフェースを実装していますが、ユーザーがio.ReadCloser
(io.Reader
とio.Closer
の両方)を実装するカスタムタイプをBody
として設定することも可能です。このcloseBody()
メソッドは、そのような場合にClose()
が確実に呼び出されるようにします。
エラーパスでのcloseBody()
の呼び出し
以前は、http.Client.Do
やhttp.Transport.RoundTrip
などの関数が、リクエストの送信前に発生するバリデーションエラーや、コネクション確立時のエラーなどで早期にリターンした場合、Request.Body
が閉じられない可能性がありました。これは、Response.Body
に対してはdefer resp.Body.Close()
という慣用句が広く使われていたのに対し、Request.Body
のクローズはあまり意識されていなかったためです。
このコミットでは、以下のような箇所でcloseBody()
が追加されました。
-
client.go
のsend
関数:t == nil
(Transportが設定されていない)req.URL == nil
(URLがnil)req.RequestURI != ""
(クライアントリクエストでRequestURIが設定されている) これらのエラーは、リクエストがネットワークに送信される前に発生する可能性のある基本的なバリデーションエラーです。ここでcloseBody()
を呼び出すことで、リクエストボディが早期に解放されます。
-
transport.go
のRoundTrip
メソッド:req.URL == nil
req.Header == nil
- サポートされていないスキーム
req.URL.Host == ""
t.getConn
(コネクション取得) エラーTransport.RoundTrip
は、実際のHTTPリクエストのライフサイクルを管理する中心的な部分です。この層で発生する様々なエラーにおいてもRequest.Body
を閉じることで、リソースリークのリスクを大幅に低減します。
-
transport.go
のpersistConn.writeLoop
:- リクエストボディの書き込み中にエラーが発生した場合。 これは、リクエストボディがストリーミングされているようなシナリオで特に重要です。書き込みが途中で失敗した場合でも、ボディが閉じられることで、関連するリソースが解放されます。
ドキュメントの更新
RoundTripper
インターフェースとClient.Do
メソッドのドキュメントが更新されたことも重要です。これにより、net/http
パッケージのユーザーは、Request.Body
がエラー時を含め、常に基盤となるTransport
によって閉じられることを明確に理解できるようになりました。これは、APIの契約を明確にし、開発者が不必要なBody.Close()
呼び出しを自分で追加するのを防ぎます。
テストの追加
TestTransportClosesBodyOnError
は、この変更の有効性を検証するための重要なテストです。このテストでは、カスタムのio.Reader
とio.Closer
を組み合わせたRequest.Body
を作成し、io.Reader
が読み取りエラーを発生させるように設定します。そして、http.DefaultClient.Do
を呼び出し、エラーが返された後でもBody.Close()
が呼び出されたことをdidClose
チャネルを通じて確認します。これにより、エラーパスでのBody
クローズが正しく機能していることが保証されます。
これらの変更により、Goのnet/http
クライアントは、より堅牢でリソース効率の良いものとなり、開発者がリクエストボディのクローズについて手動で考慮する必要があるケースが減少しました。
関連リンク
- Go Issue #6981: https://github.com/golang/go/issues/6981
- Go CL 85560045: https://golang.org/cl/85560045
参考にした情報源リンク
- Go
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go
io
パッケージのドキュメント: https://pkg.go.dev/io - Stack Overflow: Go http.Response.Body.Close() and defer: https://stackoverflow.com/questions/17948827/go-http-response-body-close-and-defer
- GitHub Issue #35015 (関連する議論): https://github.com/golang/go/issues/35015
- Go言語におけるHTTPクライアントのベストプラクティスに関する記事 (一般的な情報源)
- 例: https://medium.com/@nate510/don-t-defer-close-on-http-response-body-in-go-19d002c29789 (これは
Response.Body
に関するものですが、Body
クローズの重要性を理解する上で参考になります) - 例: https://blog.golang.org/http-client (Go公式ブログのHTTPクライアントに関する記事)
- 例: https://medium.com/@nate510/don-t-defer-close-on-http-response-body-in-go-19d002c29789 (これは
[インデックス 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.Body
がio.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.Reader
とio.Closer
インターフェース
Go言語の標準ライブラリには、データの読み書きやリソースのクローズを抽象化するための基本的なインターフェースが定義されています。
io.Reader
:Read(p []byte) (n int, err error)
メソッドを持つインターフェースです。任意のデータソースからバイト列を読み出す操作を抽象化します。http.Request.Body
やhttp.Response.Body
はこのインターフェースを実装しており、それぞれリクエストやレスポンスのボディからデータを読み出すために使用されます。io.Closer
:Close() error
メソッドを持つインターフェースです。ファイル、ネットワークコネクション、メモリバッファなど、使用後に解放する必要があるリソースをクローズする操作を抽象化します。http.Response.Body
はio.ReadCloser
(io.Reader
とio.Closer
の両方を埋め込んだインターフェース)を実装しているため、読み出し後に必ずClose()
を呼び出す必要があります。
リソース管理とコネクション再利用の重要性
- リソースリーク:
io.Closer
を実装するリソース(ファイルディスクリプタ、ネットワークソケットなど)を適切に閉じないと、それらのリソースがシステムに解放されずに残り続け、最終的にはシステムリソースの枯渇を引き起こす可能性があります。これは、アプリケーションのパフォーマンス低下やクラッシュにつながります。 - コネクション再利用 (Keep-Alive): HTTP/1.1では、複数のリクエスト/レスポンスを同じTCPコネクション上で送受信できるKeep-Alive機能が導入されました。これにより、新しいTCPコネクションを確立するオーバーヘッドが削減され、HTTP通信の効率が向上します。
net/http
パッケージは、この機能を活用するためにコネクションプールを管理しています。しかし、Response.Body
が完全に読み込まれず、かつ閉じられない場合、そのコネクションはプールに再利用のために戻されず、結果として新しいコネクションが確立され続けることになり、パフォーマンスが低下します。
このコミットは、これらの問題を解決するために、Request.Body
についても同様に、エラー時でも確実にクローズするメカニズムを導入しています。
技術的詳細
このコミットの主要な変更点は、net/http
パッケージ内の複数の箇所で、Request.Body
がio.Closer
インターフェースを実装している場合に、エラーパスにおいてもClose()
メソッドが呼び出されるように修正されたことです。
具体的には、以下のシナリオでRequest.Body
が閉じられるようになりました。
-
http.Client.Do
およびsend
関数内での初期エラーチェック:req.URL
がnil
の場合req.RequestURI
がクライアントリクエストで設定されている場合http.Client.Transport
またはhttp.DefaultTransport
がnil
の場合 これらの初期バリデーションエラーが発生した場合、リクエストが実際に送信される前にreq.Body.Close()
が呼び出されるようになりました。
-
http.Transport.RoundTrip
内でのエラー:req.URL
がnil
の場合req.Header
がnil
の場合- サポートされていないプロトコルスキームの場合
- リクエストURLに
Host
が含まれていない場合 - コネクションの取得に失敗した場合
Transport
のRoundTrip
メソッドは、HTTPリクエストの実際の送信を担当します。このメソッド内で上記のようなエラーが発生した場合、リクエストボディが閉じられるようになりました。
-
persistConn.writeLoop
内での書き込みエラー:- 永続的なコネクション(
persistConn
)がリクエストボディの書き込み中にエラーを検出した場合、そのリクエストに関連付けられたボディが閉じられるようになりました。
- 永続的なコネクション(
これらの変更は、http.Request
構造体に新しく追加されたプライベートメソッドcloseBody()
を通じて行われます。このメソッドは、r.Body
がnil
でない場合に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.Body
がTransport
によって閉じられることが追記されました。--- 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
メソッドのドキュメントが更新され、body
がio.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.Request
にcloseBody()
というヘルパーメソッドを導入し、net/http
パッケージ内の様々なエラーハンドリングパスでこのメソッドを呼び出すようにした点です。
func (r *Request) closeBody()
このメソッドは非常にシンプルですが、その役割は重要です。
func (r *Request) closeBody() {
if r.Body != nil {
r.Body.Close()
}
}
このメソッドは、Request
のBody
フィールドがnil
でない場合にのみ、そのClose()
メソッドを呼び出します。Request.Body
はio.Reader
インターフェースを実装していますが、ユーザーがio.ReadCloser
(io.Reader
とio.Closer
の両方)を実装するカスタムタイプをBody
として設定することも可能です。このcloseBody()
メソッドは、そのような場合にClose()
が確実に呼び出されるようにします。
エラーパスでのcloseBody()
の呼び出し
以前は、http.Client.Do
やhttp.Transport.RoundTrip
などの関数が、リクエストの送信前に発生するバリデーションエラーや、コネクション確立時のエラーなどで早期にリターンした場合、Request.Body
が閉じられない可能性がありました。これは、Response.Body
に対してはdefer resp.Body.Close()
という慣用句が広く使われていたのに対し、Request.Body
のクローズはあまり意識されていなかったためです。
このコミットでは、以下のような箇所でcloseBody()
が追加されました。
-
client.go
のsend
関数:t == nil
(Transportが設定されていない)req.URL == nil
(URLがnil)req.RequestURI != ""
(クライアントリクエストでRequestURIが設定されている) これらのエラーは、リクエストがネットワークに送信される前に発生する可能性のある基本的なバリデーションエラーです。ここでcloseBody()
を呼び出すことで、リクエストボディが早期に解放されます。
-
transport.go
のRoundTrip
メソッド:req.URL == nil
req.Header == nil
- サポートされていないスキーム
req.URL.Host == ""
t.getConn
(コネクション取得) エラーTransport.RoundTrip
は、実際のHTTPリクエストのライフサイクルを管理する中心的な部分です。この層で発生する様々なエラーにおいてもRequest.Body
を閉じることで、リソースリークのリスクを大幅に低減します。
-
transport.go
のpersistConn.writeLoop
:- リクエストボディの書き込み中にエラーが発生した場合。 これは、リクエストボディがストリーミングされているようなシナリオで特に重要です。書き込みが途中で失敗した場合でも、ボディが閉じられることで、関連するリソースが解放されます。
ドキュメントの更新
RoundTripper
インターフェースとClient.Do
メソッドのドキュメントが更新されたことも重要です。これにより、net/http
パッケージのユーザーは、Request.Body
がエラー時を含め、常に基盤となるTransport
によって閉じられることを明確に理解できるようになりました。これは、APIの契約を明確にし、開発者が不必要なBody.Close()
呼び出しを自分で追加するのを防ぎます。
テストの追加
TestTransportClosesBodyOnError
は、この変更の有効性を検証するための重要なテストです。このテストでは、カスタムのio.Reader
とio.Closer
を組み合わせたRequest.Body
を作成し、io.Reader
が読み取りエラーを発生させるように設定します。そして、http.DefaultClient.Do
を呼び出し、エラーが返された後でもBody.Close()
が呼び出されたことをdidClose
チャネルを通じて確認します。これにより、エラーパスでのBody
クローズが正しく機能していることが保証されます。
これらの変更により、Goのnet/http
クライアントは、より堅牢でリソース効率の良いものとなり、開発者がリクエストボディのクローズについて手動で考慮する必要があるケースが減少しました。
関連リンク
- Go Issue #6981: https://github.com/golang/go/issues/6981
- Go CL 85560045: https://golang.org/cl/85560045
参考にした情報源リンク
- Go
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go
io
パッケージのドキュメント: https://pkg.go.dev/io - Stack Overflow: Go http.Response.Body.Close() and defer: https://stackoverflow.com/questions/17948827/go-http-response-body-close-and-defer
- GitHub Issue #35015 (関連する議論): https://github.com/golang/go/issues/35015
- Go言語におけるHTTPクライアントのベストプラクティスに関する記事 (一般的な情報源)
- 例: https://medium.com/@nate510/don-t-defer-close-on-http-response-body-in-go-19d002c29789 (これは
Response.Body
に関するものですが、Body
クローズの重要性を理解する上で参考になります) - 例: https://blog.golang.org/http-client (Go公式ブログのHTTPクライアントに関する記事)
- 例: https://medium.com/@nate510/don-t-defer-close-on-http-response-body-in-go-19d002c29789 (これは