Overview
Go の http.Request
を二度読み込みしたいケースでハマったのでその備忘録です。
どういうケースか?
API を書いていると認証・認可等のAPI全体に関わる共通の前処理を middleware で行い、ユースケースの処理を後続で行うというパターンがよくあると思います。
この際、大体のケースはリクエストヘッダーの情報のみで処理を行い、Body 本体に触ることは少ないかと思いますが、Body の中の情報も認証に利用するケース(*1) に遭遇したときに、前処理で Request Body を読み込み、Close してしまうと後続の本処理で Body を読み込んでも中身が空、という状況に出くわした、というケースです。
サンプルは以下のようなケースです。この場合 HogeHandler で Body を Read してもすでに Close された Request Body を Read してるので Body は空です。
これに対応するために middleware で利用する http Request を別の変数に渡して処理したりもしましたが、http Request はポインタ型で指し示す実体が同じなので、変数を入れ替えて Body を読み込んでもポインタなので original のリクエストのBodyも読み込みが完了してしまいます。
// middleware.go func VerifyToken(http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { b, err := io.ReadAll(r.Body) if err := nil { // error handling } defer r.Body.Close() // 読み込んだ body の byte 配列を使っての処理 } } // handler.go func HogeHandler(w http.ResponseWriter, r *http.Request) { var v Hoge{} decorder := json.NewDecoder(r.Body) if err := decorder.Decode(&v); err != nil { // error handling } // ※ output -> v is empty } // main.go mux := http.NewServeMux() mux.HandleFunc("/hoge", VerifyToken(HogeHandler))
*1. 今回のユースケースは LINE Bot を実装してときのケースです。LINE MessagingAPI では webhook に登録した Endpoint において LINE MessagingAPI からのアクセスであることを検証することが推奨されており、署名を検証する ときに Body の中身も検証対象に含めています。
結論: Clone してから Body に詰め直す
以下の Stack Overflow 回答が書いてあります。
上記の発生した対応への修正としては以下です。
- middleware ではリクエストを Clone してこの Clone したものを使用する。
- Clone したリクエストから Body を読み込む。(このとき context が同じ場合は元の Request の Body も読み込まれてしまっている)
- Clone したリクエスト Body の値を middleware では利用する。
- 読み込んだリクエストBodyを再度 io.Reader に変換して、Clone 元のリクエスト(original) の Body に差し込む。
サンプルコードは以下です。
// middleware.go func VerifyToken(http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, origReq *http.Request) { ctx := origReq.Context() copy := origReq.Clone(ctx) b, err := io.ReadAll(copy.Body) if err := nil { // error handling } defer copy.Body.Close() origReq.Body = io.NopCloser(bytes.NewBuffer(b)) // 読み込んだ byte を元のリクエストの Body に入れ直す } }
まとめ
Stack Overflow に助けられましたが、1 Request の中で Body を2度読み込むという、珍しいユースケースに当たったので http.Request のコピーとその取り回しの仕方の勉強になりました。