emahiro/b.log

Drastically Repeat Yourself !!!!

http.Request をコピーする

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 回答が書いてあります。

stackoverflow.com

上記の発生した対応への修正としては以下です。

  • 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 のコピーとその取り回しの仕方の勉強になりました。