Carpe Diem

備忘録

Go でサーバレスポンスの内容を表示

概要

開発中にサーバレスポンスの内容を表示したい時があると思いますが

  • クライアントが受け取ったレスポンス
  • サーバが送ったレスポンス

のそれぞれをログなどで表示する方法を紹介します。

環境

  • go 1.14.3

クライアントが受け取ったレスポンスを表示

クライアントでは受け取ったレスポンスはio.ReadCloserです。
なので一度読み込むと以降は読み込めなくなるのでそこだけ注意が必要です。

io.Reader系で実処理ではreadする処理がほぼ必ず入るので、io.TeeReader を使うと良いです。

f:id:quoll00:20200603013559p:plain

ref: Goのioパッケージのメソッドを図示 - Carpe Diem

io.TeeReaderはreadすると同時にio.Writerの方にストリームデータが流れるので、これをPrintすれば良いです。
今回は標準エラー出力に出します。

コード

type Foo struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
}

func main() {
        resp, err := http.Get("http://localhost:8080")
        if err != nil {
                log.Fatal(err)
        }
        defer resp.Body.Close()

        r := io.TeeReader(resp.Body, os.Stderr)  // here

        var foo Foo
        err = json.NewDecoder(r).Decode(&foo)
        if err != nil {
                log.Fatal(err)
        }

        fmt.Println(foo)
}

サーバが送ったレスポンスを表示

今度は逆にサーバ側が送るレスポンスを表示する方法です。

前提として

  • net/httpのみでWebフレームワークは使わない
  • ミドルウェアとしてハンドリングする

とします。

イメージとしてはレスポンスを書き込む際にio.MultiWriterを使って

f:id:quoll00:20200603012054p:plain

ref: Goのioパッケージのメソッドを図示 - Carpe Diem

レスポンスとデバッグログ用のio.Writerの両方に書きこみますが、レスポンスで扱っているhttp.ResponseWriterは単純なio.Writerではないので少し工夫が必要です。

コード

先にコードを紹介すると以下です。

type Foo struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

type wrapper struct {
    http.ResponseWriter
    mw io.Writer
}

func (w *wrapper) Write(b []byte) (int, error) {
    return w.mw.Write(b)
}

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        buf := bytes.NewBuffer(nil)
        wrap := &wrapper{w, io.MultiWriter(w, buf)}
        next.ServeHTTP(wrap, r)
        fmt.Println("[DEBUG]", buf.String())  // here
    })
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/", middleware(fooHandler()))
    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatal(err)
    }
}

func fooHandler() http.HandlerFunc {
    body := Foo{"Alice", 20}

    return func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(body)
    }
}

ポイント

ポイントとしては

  • io.MultiWriterで通常のレスポンス、デバッグログ用のbufferの両方に書き込む
  • wrapperでWrite()する時にio.MultiWriterで生成されたio.Writerを使う

です。

wrap := &wrapper{w, io.MultiWriter(w, buf)}
next.ServeHTTP(wrap, r)

でwrapperをハンドラーの書き込み先としてセットしているので、ハンドラーがWrite()する際はoverrideした

func (w *wrapper) Write(b []byte) (int, error) {
    return w.mw.Write(b)
}

こちらが呼ばれます。
なのでこの時にio.MultiWriterを使うことで、実レスポンスとデバッグログの両方に書き込むことができます。

サンプルコード

今回書いたサンプルコードはこちら

github.com

まとめ

ストリームデータであるサーバレスポンスを、io.TeeReaderio.MultiWriterを活用してデバッグ表示する方法を紹介しました。