🐥

Golang の http.Request は送信後 body を close することがあるため作り方によっては気をつけないといけないっぽ

2022/05/12に公開約2,200字2件のコメント

内容に誤りと不明点があるようなので追記します

matuuさんにこの記事のコードでは記事のような問題が発生しないというコメントをいただきました。

事実簡易的に実験コードを書いたところ問題がなかったことを確認しました。

ただ、手元の環境ではBodyがcloseされた、という状況もあったため、実際にどのようなコードだとcloseされているのか等を確認した上で後日追記します。

たぶんhttp.DefaultClientとかhttp.NewRequestを使ってなかった気がする……


コードは実行したわけではなく参照で持ってきているだけなので間違っている部分があったらご了承を

GolangでHTTPリクエストを投げて、状況次第で再送したいというケースがあるとする

url := "https://example.com"
body := createBody() // いい感じにbodyを用意していると仮定する
request, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
request.Header.Set("Content-Type", "application/json")
response, _ := http.DefaultClient.Do(request)
for retry(response) {
	response, _ = http.DefaultClient.Do(request)
}

こんな感じを想定する。
これは、再送しない場合うまく動いてくれるだろうが、再送するとリクエストのbodyがnilになっていて失敗してしまう。

これは(*http.Client).Do(req *http.Request)が送信後に req.Body.Close() を読んでいるからだ(ソースコードのここの部分)
ポインタ経由で close() されているわけである。

なのでこうしてみる。結論から言うとこれも失敗する

url := "https://example.com"
body := createBody() // いい感じにbodyを用意していると仮定する
request, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
request.Header.Set("Content-Type", "application/json")
response, _ := http.DefaultClient.Do(request.Clone(context.TODO))
for retry(response) {
	response, _ = http.DefaultClient.Do(request.Clone(context.TODO))
}

なぜなら、 func (r *Request) Clone(ctx context.Context)はbody要素をコピーしてくれないのである。罠かよ。

url := "https://example.com"
body := createBody() // いい感じにbodyを用意していると仮定する
request, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
request.Header.Set("Content-Type", "application/json")
defer request.Body.Close()
r := request.Clone(context.TODO())
r.Body = ioutil.NopCloser(bytes.NewReader(body)
response, _ := http.DefaultClient.Do(r)
for retry(response) {
	r = request.Clone(context.TODO())
	r.Body = ioutil.NopCloser(bytes.NewReader(body)
	response, _ = http.DefaultClient.Do(r)
}

Bodyを別途コピーしてあげると通るようになる。
本体(?)のBodyはいらないかもしれないが、まあそれはリファクタリングのタイミングで除いてあげればいいと思います。


もしかしたら httpClient の作り方次第では body が close() されないかもしれないが、transportあたりまで突っ込みそうになったのでやめて次善策を取ることにした。
もっと簡単な方法があるよ!等を知っている方いらっしゃいましたら教えてください。わからなかったんですよ……

Discussion

NewRequest時に bytes.NewReader (つまるところ *bytes.Reader )としてbodyを引き渡す場合はio.NopCloser経由で渡すのでCloseされても実際には実行されず、何度retryしても同じbodyが渡されるはずですね。
上記リンク先前後の該当する型でないのであれば、同様にio.NopCloser経由で渡すことでCloseを回避できるのではないかと思います。

コメントありがとうございます。
確かに確認したところ問題が発生しなかったので、前提条件や確認内容に問題がありました。

記事を消すのは不誠実だと考えたので追記で対応しています。ありがとうございました。

ログインするとコメントできます