エラーの内容
GoでWebサーバーを構築していると、コンソールにこのような煩わしいメッセージが表示されることがよくあります。
2023/10/27 10:00:00 http: multiple response.WriteHeader calls
サーバーがクラッシュしたわけではなく、動作は継続します。しかし、このログはリクエストハンドラーが「過去を変える」という不可能な操作を行おうとしていることを示す警告です。Goでは、一度HTTPステータスコードを送信すると、そのリクエストに対して後から変更することはできません。
根本的な原因
net/httpパッケージにおいて、http.ResponseWriterは一方通行です。一度ヘッダーが送信されると、それを取り消すことはできません。このエラーは通常、コードがステータスコードを設定したり、レスポンスボディに書き込んだりする処理を複数回実行しようとした際のロジックの不備から発生します。
1. 「return」の書き忘れ
原因の9割がこれです。エラーを検出し、http.Error()を呼び出した後、関数の実行を継続させてしまうケースです。http.Errorは内部でWriteHeaderを呼び出します。その後のコードで別のWriteHeaderやWriteが実行されると、Goは警告を発生させます。
2. 暗黙的ヘッダーと明示的ヘッダー
Goは親切に設計されています。w.WriteHeader()を呼び出す前にw.Write()を呼び出すと、Goは200 OKを意図していると判断し、即座にそのヘッダーを送信します。データを書き込んだ後に別のステータスコードを設定しようとしても、手遅れなのです。
修正方法
シナリオA:returnの欠落
よくある間違いを見てみましょう。idが欠落している場合、コードは400エラーを送信しますが、処理を停止しません。そのまま続行し、200 OKのロジックまで到達してしまいます。
func handler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "IDがありません", http.StatusBadRequest)
// ここで実行が継続してしまいます!
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("成功"))
}
**修正方法:**ガード句を使用します。エラーレスポンスを送信した直後には必ずreturnを呼び出し、その時点で関数の実行を終了させてください。
func handler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "IDがありません", http.StatusBadRequest)
return // 正解:ここで関数を終了する
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("成功"))
}
シナリオB:呼び出し順序
データがネットワークに送信され始めると、ステータスコードを変更することはできません。以下のコードは、"処理中..."が書き込まれた瞬間に200 OKが送信されるため、エラーになります。
func handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("処理中..."))
// ヘッダーが既に送信されているため、ここでエラーが発生します
w.WriteHeader(http.StatusCreated)
}
**修正方法:**ボディを1バイトでも書き込む前に、ステータスコードとヘッダーを設定してください。json.NewEncoder().Encode()を使用する場合も、先にヘッダーを設定します。
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"status": "完了"})
}
シナリオC:ミドルウェアでの二重書き込み
ミドルウェアは多くの場合、next.ServeHTTP(w, r)の呼び出しをラップします。次のハンドラーを呼び出した後にミドルウェアがレスポンスへの書き込みを行うと、内部のハンドラーが既に送信した内容と衝突する可能性があります。
**修正方法:**ハンドラーの実行後にレスポンスを変更する必要がある場合は、カスタムのResponseWriterラッパーを使用して出力をバッファリングするか、クライアントに送信される前にステータスコードをキャプチャする必要があります。
動作確認
実際に修正されたかどうかをどのように確認すればよいでしょうか?推測に頼らず、以下のチェックを行ってください。
- **ログを監視する:**エラーを引き起こしたアクションを実行します。コンソールに何も表示されなければ、おそらく問題は解決しています。
- **Curlで検査する:**`curl -v http://localhost:8080/endpoint`を使用します。`< HTTP/1.1 200 OK`のような行を確認してください。ステータス行が正確に1つだけ表示され、予期しないボディ内容が含まれていないことを確認します。
- **NewRecorderでテストする:**ユニットテストで`httptest`パッケージを使用します。これにより`Result().StatusCode`を検査でき、条件分岐を通じてロジックが正しく流れているかを確認できます。
ベストプラクティス
- **早期リターン(Return Early):**「早期リターン」パターンを採用しましょう。コードのネストを浅く保ち、二次的なロジックが誤って実行されるのを防ぐことができます。
- **レスポンス箇所を1つに絞る:**ハンドラー内で最終的な成功レスポンスを書き込む場所を1か所にまとめるように努めてください。
- **コードを静的解析する:**`golangci-lint`を使用してください。実行パスが複数の書き込みにつながる可能性がある箇所を検出できる場合があります。
- **ヘッダーの順序:**常に「1. ヘッダーの設定、2. WriteHeader、3. ボディの書き込み」の順序を守ってください。

