EchoのNewHTTPErrorにおける型ごとのレスポンスについて

published
2024-07-11
tags

Echoで、NewHTTPErrorを用いてエラーレスポンスを返却する際、第二引数に渡す値の型によってレスポンス形式が変化します。
stringを第二引数に渡すと、{message: ""}に含められて返却されます。が、他の型の場合は{message:""}には埋め込まれなかったりします。

今回は上記処理を行っているEchoのコードと、その対応について備忘録を兼ねてかきました。

環境

  • go 1.18.1
  • echo 4.12.0

準備

まずは、EchoのQuick Startを参考に、Hello Worldが返却されるまでをササっと構築します。

ドキュメントにあるように以下コマンドを入力します。

$ mkdir myapp && cd myapp
$ go mod init myapp
$ go get github.com/labstack/echo/v4

hello worldを返却するエンドポイントを実装します。

package main

import (
	"net/http"
	"github.com/labstack/echo/v4"
)

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":1323"))
}

サーバを起動してHello Worldが返却されるか確認します。

$ go run server.go

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.12.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\       
⇨ http server started on [::]:1323
$ curl http://localhost:1323
Hello, World!

いい感じですね。
ではNewHTTPErrorを用いて、エラーレスポンスを返却するエンドポイントに変更してリクエストしてみます。(一行変えるだけですが)
httpエラーは何でもいいですが、StatusUnauthorizedにしたいと思います。

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

	e.GET("/", func(c echo.Context) error {
		return echo.NewHTTPError(http.StatusUnauthorized, "エラーなのだ")
		// return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":1323"))
}

サーバを起動しなおし、再度リクエストしてみます。

$ curl http://localhost:1323
{"message":"エラーなのだ"}

上記の通り"エラーなのだ"、をNewHTTPErrorの第二引数に渡して返すとEchoのデフォルトのエラーハンドリング君がmapのmessageに含めて返却してくれます。

次に、この第二引数に渡す値をstringではなく、stringの配列にしてみます。

	e.GET("/", func(c echo.Context) error {
		return echo.NewHTTPError(http.StatusUnauthorized, []string{"エラーなのだ", "エラーなのだ2"})
		// return c.String(http.StatusOK, "Hello, World!")
	})

結果としては、mapのmessageに配列が含まれて返ってきてほしいですね。

$ curl http://localhost:1323
["エラーなのだ","エラーなのだ2"]

なんでなのだ...配列の場合messageに含まれて返却されるのではなく、そのまま返ってきました。試しに他の型の値を試してみても、messageに含まれるのはstringのみでした。

なぜ

これは、EchoのDefaultHTTPErrorHandlerの処理を覗みるとわかりました。

messageに含まれるのは、上記のhe.Message.(type)のswitchしている部分で、stringの場合に、messageに埋め込むようにして返しているからです。

	switch m := he.Message.(type) {
	case string:
		if e.Debug {
			message = Map{"message": m, "error": err.Error()}
		} else {
			message = Map{"message": m}
		}
	case json.Marshaler:
		// do nothing - this type knows how to format itself to JSON
	case error:
		message = Map{"message": m.Error()}
	}

Note

echoの4.12.0の情報です。他バージョンは確認していない+今後のバージョンによっては挙動が変わる可能性がありますので、その時のバージョンに合った情報を参照ください

どうする?

ドキュメントにもある方法で、カスタムハンドラーを定義して回避するのが一つの手です。

今回は、stringの配列の時のみ独自にレスポンスを返すように変更します。

package main

import (
	"net/http"
	"github.com/labstack/echo/v4"
)

func customHTTPErrorHandler(err error, c echo.Context) {
	he, ok := err.(*echo.HTTPError);
	if !ok {
		c.Echo().DefaultHTTPErrorHandler(err, c)
		return
	}
	switch m := he.Message.(type) {
	case []string:
		err = c.JSON(he.Code, map[string]interface{}{"message": m})
	default:
		c.Echo().DefaultHTTPErrorHandler(err, c)
	}
}

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

	e.GET("/", func(c echo.Context) error {
		return echo.NewHTTPError(http.StatusUnauthorized, []string{"エラーなのだ", "エラーなのだ2"})
		// return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":1323"))
}

定義するHandlerでは、stringの配列の時のみ、独自でレスポンスを返却し、それ以外
は元のDefaultのエラーハンドラーに処理を委譲します。
また、カスタムで定義したHandlerをe.HTTPErrorHandlerに渡します。
これで、stringの配列の場合でもmessageに埋め込まれて返却されるはずです。

実際に確認してみます。

$ curl http://localhost:1323
{"message":["エラーなのだ","エラーなのだ2"]}

想定通りのエラーレスポンスが返却されました。

Warning

上記の対応は下手すると対応する型ごとにcaseで分岐しないといけなくなるため、まとめで紹介しているerrorのインターフェースを拡張する方法をお勧めします

まとめ

何気なくNewHTTPErrorを用いてエラーを返していて、フロントで受け取る際に上記の差異があり今回調べてみましたが、同様のことで悩んだ方の参考程度になれれば幸いです。

また、個人的に(個人的に!!!)Echoのドキュメントはあっさり塩味すぎて、カスタマイズ性を駆使して使用するには物足りない感があるなと感じました。

上記ソースコードはこちらにあげてあります。

PS:
記事書いた後に気づいたので今回は試しませんが、switchにerrorでの分岐があるので、errorインターフェースを拡張してもよいかもしれません。
(というより、それが一番良いかも)