LoginSignup
75
64

More than 5 years have passed since last update.

echoのAPIサーバ実装とエラーハンドリングの落とし穴

Last updated at Posted at 2016-05-05

基本的な実装方法について

公式ドキュメントから転記

package main

import (
    "net/http"
    "strconv"

    "github.com/labstack/echo"
    "github.com/labstack/echo/engine/standard"
    "github.com/labstack/echo/middleware"
)

type (
    user struct {
        ID   int    `json:"id"`
        Name string `json:"name"`
    }
)

var (
    users = map[int]*user{}
    seq   = 1
)

//----------
// Handlers
//----------

func createUser(c echo.Context) error {
    u := &user{
        ID: seq,
    }
    if err := c.Bind(u); err != nil {
        return err
    }
    users[u.ID] = u
    seq++
    return c.JSON(http.StatusCreated, u)
}

func getUser(c echo.Context) error {
    id, _ := strconv.Atoi(c.Param("id"))
    return c.JSON(http.StatusOK, users[id])
}

func updateUser(c echo.Context) error {
    u := new(user)
    if err := c.Bind(u); err != nil {
        return err
    }
    id, _ := strconv.Atoi(c.Param("id"))
    users[id].Name = u.Name
    return c.JSON(http.StatusOK, users[id])
}

func deleteUser(c echo.Context) error {
    id, _ := strconv.Atoi(c.Param("id"))
    delete(users, id)
    return c.NoContent(http.StatusNoContent)
}

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

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

    // Routes
    e.POST("/users", createUser)
    e.GET("/users/:id", getUser)
    e.PUT("/users/:id", updateUser)
    e.DELETE("/users/:id", deleteUser)

    // Start server
    e.Run(standard.New(":1323"))
}

これでOKではない!!

これはあくまでも、Routingの仕方とHandlerの実装方法を書いているだけで例外パターンは想定されていません。
実際にサービスを作るときにはこれだけでは足りないのです。

ではどうするか

1. エラーハンドラをカスタマイズする

APIサーバを構築する場合、正常でも異常でも同じ形式で結果を返すべきだと考えます。
(正常系がJSONで返すなら、異常でもJSONを返すべき)

しかし、デフォルトのエラーハンドラはtext形式 (echo.Context.String) で
エラーレスポンスを返します。
これは、echoが全ての形式を統一するのではなく、ハンドラ毎にレスポンスの形式を設定できるようになっているからです。

例えば、公式ドキュメントの使用例の場合

GET_(/users/1)にアクセス
{
    name: "foobar"
}

などになるのに対して、エラーの場合、次のようになります。

Routingされてないエンドポイントにアクセス(例:/contents/1)
Not Found

これを解消するためにカスタマイズしたエラーハンドラを設定します。

type APIError struct {
    Code int
    Message string
}

func JSONErrorHandler(err error, context *echo.Context) {
    code := http.StatusInternalServerError
    msg := http.StatusText(code)

    if he, ok := err.(*HTTPError); ok {
        code = he.Code
        msg = he.Message
    }
    if e.debug {
        msg = err.Error()
    }

    var apierr APIError
    apierr.Code    = code
    apierr.Message = msg

    if !c.Response().Committed() {
        c.JSON(code, apierr)
    }
    e.logger.Debug(err)
}

これをRouter( main() )で設定します。

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

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

    // 今回追加したところ
    e.SetHTTPErrorHandler(JSONErrorHandler)

    // Routes
    e.POST("/users", createUser)
    e.GET("/users/:id", getUser)
    e.PUT("/users/:id", updateUser)
    e.DELETE("/users/:id", deleteUser)

    // Start server
    e.Run(standard.New(":1323"))
}

2. エラーは error で返すだけでは不十分

エラーを返す場合は

if err := c.Bind(u); err != nil {
    return err
}

のような箇所を

if err := c.Bind(u); err != nil {
    var apierr APIError
    apierr.Code    = 100
    apierr.Message = "invalid request"

    c.JSON(htt.StatusBadRequest, apierr)
    return err
}

とするか
もしくはWrapperを作った方がいいです

func JSONError(c *echo.Context, status int err error, code int, msg string) error {
    var apierr APIError
    apierr.Code    = code
    apierr.Message = msg

    c.JSON(status, apierr)
    return err
}

if err := c.Bind(u); err != nil {
    return JSONError
}

先ほどの作ったエラーハンドラでは、HTTP Statusからcodeとmessageを生成していました。
この場合であれば、先ほどのエラーハンドラでだけでも問題ありません。
しかし、実際APIを作る場合は独自のエラーコードとメッセージを返したり、
ユーザ向けのエラーメッセージとその詳細を返す場合があります。
この場合だと、先ほど作ったエラーハンドラでは処理が足りません。

エラー毎にきちんとエラーレスポンスを書くのにはまだ利点があります。
仮にエラーハンドラ内で独自のエラーコードや詳細など付随する情報を自動生成したとしても
特定のエラーの場合には例外的に通常とは異なるエラーを返したい場合があります。
このような場合に柔軟に対応するためにも個別にエラーレスポンスを返せるようにしておいたほうがいいのです。

もう一つ注目すべきなのが改修後も err を返しているところです。
これは echo.Context.JSONecho.Context.String がHTTP Statusに関係なく nil を返してしまい、Unitテストの弊害になってしまうためです。
Unitテストに関しては別の機会に書きます。

labstack/echo の構築例はパターンが多く、とても良く書かれていますが、
それでも機能説明に最低限のところしか書かれていません。

実際に使用する場合は関数をよく理解したうえで使いましょう。

75
64
1

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
75
64