sano11o1

echoのHTTPErrorHandlerが重複して呼ばれる挙動の対策

公開日:

はじめに

echoを使ってAPIを開発している際に、1リクエストに対してHTTPErrorHandlerがなぜか2回呼ばれる挙動に遭遇したので、その原因と対策について書き留めておく。

HTTPErrorHandler の使い方

ハンドラがerrorを返したとき呼ぶメソッドを定義できる。
エラーをログに出力したり、Sentry等のエラートラッキングのシステムにエラーを送るコードを仕込めたりする。
参考

e.Use(xxx.HogeMiddleware())

e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  return func(c echo.Context) error {
    return errors.New("エラーです")
  }
})

func customHTTPErrorHandler(err error, c echo.Context) {
    c.Logger().Error(err)
    sentry.CaptureException(err)
}

e.HTTPErrorHandler = customHTTPErrorHandler


上記のコードでログが2回出力され、Sentryに2回エラーが送信されていた。
期待している動作はログに1回出力、Sentryに1回送信することなので、どこかで余分にHTTPErrorHandlerが呼ばれていることになる。

原因

HogeMiddlewareの中でc.Error()メソッドを呼び出し、HTTPErrorHandlerが呼ばれていた。
echoのMiddlewareではハンドラ側でerrorが発生していたら、c.Errorを呼ぶ構成になっていることが多い。

func HogeMiddleware(opts ...Option) echo.MiddlewareFunc {
  return func(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        if err := next(c); err != nil { // 次のMiddlewareもしくはハンドラの呼び出し
          c.Error(err)
          return err
        }
        // HogeMiddleware特有の処理
    }
  }
}


Errorメソッド はHTTPErrorHandlerを呼び出す。
副作用としてHTTPレスポンスをクライアントに送信(コミット)する。

// Error invokes the registered global HTTP error handler. Generally used by middleware.
// A side-effect of calling global error handler is that now Response has been committed (sent to the client) and
// middlewares up in chain can not change Response status code or Response body anymore.
// Avoid using this method in handlers as no middleware will be able to effectively handle errors after that.
Error(err error)


対策

Commit済みの場合は、Response().Committedの値がTrueに更新される。
Response().Committedの値を見て、Commit済みであれば早期Returnすることで重複呼び出しを防げる

func customHTTPErrorHandler(err error, c echo.Context) {
   if context.Response().Committed {
      return
    }
    c.Logger().Error(err)
    sentry.CaptureException(err)
}


実はデフォルトのHTTPErrorHandler ではCommit済みであれば早期Returnする実装になっている

func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
	if c.Response().Committed {
		return
	}
}