1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Go] echoフレームワークで Response Body を取得するアレな方法

Last updated at Posted at 2020-10-30

まえがき

Goのechoフレームワークでトレースログとエラーコードをロギングしようとしたら、エラーコードを決定するロジックが複数あり、レスポンスから取得するのが一番影響が少ないことに気づいてしまった困った人向け

エラー時のみハンドリングしたかったり、トレースログを含める必要があったので、 middleware/body-dump をカスタマイズする方向でミドルウェアを書いた。

トリッキーな方法だと思うので、単純に Response Body を取得したいだけであれば middleware/body-dump を使うことを推奨

仕様とかそこらへん

Response Body を取得したいけど Response は stream に書き込まれてるっぽくって後から取得しようとしてもできなかったので、Context.Response().Writer をトラップ付き Writer への参照に書き換えて stream と memory の両方へ書き込まれるようにした。

echo/middleware/body_dump.go - GitHub をベースに車輪の再発明カスタマイズ。

必要なときにハンドラを設定して Response が書き込まれたら設定しておいた handler が呼ばれる仕様とした。

コアとなる部分の実装

package handler

import (
	"io"
	"fmt"
	"bytes"
	"bufio"
	"net"
	"net/http"

	"github.com/labstack/echo"
)

// ここから
type(
	bodyDumpResponseWriter struct {
		io.Writer
		http.ResponseWriter
	}
)

func (w *bodyDumpResponseWriter) WriteHeader(code int) {
	w.ResponseWriter.WriteHeader(code)
}

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

func (w *bodyDumpResponseWriter) Flush() {
	w.ResponseWriter.(http.Flusher).Flush()
}

func (w *bodyDumpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	return w.ResponseWriter.(http.Hijacker).Hijack()
}
// ここまで middleware/body-dump の内容まま

// 以降が記事の処理の実体
func ResponseHandler(c echo.Context, err error) {
    // Body を memory へ dump する為の Buffer
	resBody := new(bytes.Buffer)

	// http.ResponseWriter と 用意した Buffer の両方へ書き込むために io.MultiWriter を使用
	mw := io.MultiWriter(c.Response().Writer, resBody)

	// Body の dump 機能付き Writer の作成
	writer := &bodyDumpResponseWriter{Writer: mw, ResponseWriter: c.Response().Writer}

	// Context の Writer を先程作成した Writer に置換
	c.Response().Writer = writer

	// Response が書き込まれた際に呼ばれる関数を set
	c.Response().After(responseHandler(c, err, resBody))
}

func responseHandler(c echo.Context, err error, resBody *bytes.Buffer) func() {
	return func() {
		// ここで resBody のパースや error の分解や Context の内容を取得する処理を定義

		fmt.Printf("%s: %s", err, resBody.String())
	}
}

使い方

package main

import (
	"net/http"
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"

	"./handler"
)

func main() {
	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.GET("/", hello)

	e.Logger.Fatal(e.Start(":1323"))
}

func hello(c echo.Context) error {
	// Response が書き込まれる直前に設定
	handler.ResponseHandler(c, nil)

	// Context が Response を書き込み終えると responseHandler() が call される
	return c.String(http.StatusOK, "Hello, World!")
}

エラーハンドラの中でエラーレスポンスを生成してる箇所とかあればその直前にセットすれば良いと思う。

// JSONを返すコードの場合はこんな感じ
handler.ResponseHandler(c, nil)
c.JSON(http.StatusInternalServerError, ...)

注意

echo - GoDoc Response.After

After registers a function which is called just after the response is written. If the Content-Length is unknown, none of the after function is executed.

今回のケースでは問題とならないが、 Response.After の注意書きに「Contents-Length が不明な場合は呼ばれないよ。」と書いてある。ファイルのアップストリームとかでは使えないようだ

参考にした資料

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?