goで http.Requestのbodyが途中でエラーを吐いてもサーバーが見落とすパターン
遭遇したこと
クライアントサイドで、多少ややこしいbodyを作る場面があり、「このbody(Reader)がもし途中でエラーを吐いたら、サーバーはちゃんとそれを検出できるのか?」とふと気になって検証をしました。
すると、かなり盲点だったことを見つけました。
サーバーサイドの実装によってはerrorに気づかないことがありえます。
コード
クライアントサイドがこういうコードだとします
func main()
res, err := http.Post("http://localhost:8080", "text/plain", &badReader{})
fmt.Printf("%+v\n", res.StatusCode)
fmt.Printf("%+v\n", err)
}
// reader returns error
type badReader struct {
}
// Read
func (s *badReader) Read(p []byte) (n int, err error) {
copy(p, []byte(`{"a":1}`))
return 9, fmt.Errorf("badReaderError")
}
問題のあるBodyをシミュレーションしています。Readしていると途中でエラーが出てしまいます。
期待としては、このBodyを処理するサーバーサイドは、どこかでエラーを捕捉してほしいものです。
そこで、よくあるサーバーサイドを書いてみました。
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
m := map[string]interface{}{}
err := json.NewDecoder(r.Body).Decode(&m)
fmt.Printf("%+v\n", m)
fmt.Printf("%+v\n", err)
fmt.Fprintf(w, "Hello")
})
期待としては、 err := json.NewDecoder(r.Body).Decode(&m)
がエラーを返してくれると、漠然と思っていました。
ところがprint結果はこうなります。
map[a:1]
<nil>
エラーが返らないのですね。
なにが起きているか
「json.Decoder」を使ってリクエストをパースすると問題が起きるという理解です。もしクライアントサイドでリーダーがエラーを返した場合であっても、サーバー側はこれを正しく読み取ってしまいます。なぜなら、「json.Decoder」は入力ストリームをパースし、その結果として一応完結したJSONを返すからです。
サーバー側が確実にエラーを把握するには、io.ReadAll(r.Body)
を使うなどして、確実にボディ全体を読み切る必要があります。
io.ReadAllすると unexpected EOF
がちゃんと返ります。
json.Decoder
がBodyを読み切らないというのは結構盲点でした。
Bodyは全部読み切るのが基本である気もします。ちょっと既存の実装を見直す必要もあるかも。。。
Discussion