どんな問題か
net/http の Get などを使ってリクエストをかけた時、返ってきたレスポンスのボディは必ず閉じなければいけない、というのは、あちこちで言われていまして、わりとよく知られていることかと思います。
もちろん、net/http のドキュメントにも、冒頭に書かれています。
The caller must close the response body when finished with it:
resp, err := http.Get("http://example.com/")
if err != nil {
// handle error
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
// ...
なので、上の net/http のサンプルコードのように、defer を使って、関数・メソッド終了時に処理するというのが定番です。
ただし、サンプルコードのように io.ReadAll(resp.Body)
を使ってすぐにレスポンスボディを読み込むことを怠り、resp.Body
を別の処理にそのまま使おうとすると、まれに問題がおきます。次のコードのような場合です。
defer resp.Body.Close()
x, err := doSomething(resp.Body)
return x, err
上のコードだと、doSomething
の処理が瞬時に終わるようならおそらく問題は起きないと思いますが、処理の間、リクエストのコネクションはつながったままなので、時間がかかるようだと接続先の方からタイムアウトでコネクションを切断されていしまい、doSomething
の処理が失敗してエラーということが起こりえます。というか、起きました・・・(悲)。
こういうエラーが返ってきました。
read tcp [xxxx:xx:xxxx:x:xxxx:xxxx:xxxx:xxxx]:55555->
[yyyy:yyy:yyy:yy::yyyy:yyy]:443: read: connection reset by peer
コードを書いた当初、このエラーは起きませんでした。
複数のリクエストを、順番に、前のリクエストの処理が終わってから、次のリクエストをかける、というやり方から、時間短縮のため、Goroutine を使って同時にリクエストをかけるように変更したところで、時折エラーが出るようになりました(時折、というのがすごく嫌)。
もともと、レスポンスボディに含まれる処理対象のデータが数メガバイトとやや大きめだったことに加え、同時にリクエストをかけるようになったことによる処理の競合やその時時の通信状況が影響したと考えられます。
以下のように修正して対処しています。
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
x, err := doSomething(bytes.NewBuffer(b))
return x, err
横着はするもんじゃないですね。自戒、自戒。