ogen の convenient error の用途と使い方をメモ

ogen を使うとログに出力される INFO がある

(ogen とは、OpenAPI の Go サーバーコード生成ツールの一つ。ogen については他の記事を参照のこと。)

ogen を使い始めるとすぐ、コード生成コマンドで以下の info がログに出ることに気づく

INFO convenient Convenient errors are not available {"reason": "operation has no \"default\" response", "at": "file:///h/o/g/e/hoge.yaml:5:5"}

Convenient Error が使えなかった、とのこと。

Convenient Error が何かは知らないが、 Convenient な Error なのだから、API Schema 全体で共通して定義できるエラーなど、なんらかコードを書く量を減らせる類の機能だと予想される。

実際どうか、調べてみた。

公式 document をみてみる

公式 document

ogen は document が充実しているとはいえない(2023/12/17 時点)のが難点だが、独自の概念であるからか Convenient Errors については記載がある

以下の 2 条件を満たしたときに使える機能らしい

  • すべての operation で同じデフォルトのレスポンスが定義されている
  • このレスポンスはapplication/jsonメディアのみを定義している

まぁまぁ厳しい条件な気もするが、そもそも endpoint によらないエラーを定義する機能なんだと考えれば納得はいく

どんな目的で使うか

ogen では interface を生成するが、その実装時、事前定義されているエラーについては一つ目の返り値で返すことが想定されている。 API Schema に 404 が定義されていれば、 一つ目の返り値で 404 に相当する構造体を返す。(これはこれで面白いが、割愛)

二つ目の返り値は error interface を満たす値だが、これはアプリケーションの想定外の場合に返すイメージ。

Convenient Error を使わない通常の生成の場合、二つ目の返り値があると、500 が返され、err.Error の中身が message として返る。

Convenient Error は、二つ目の返り値があるときの挙動に共通してロジックを適用できる機能だ。 具体的な用途でパッと思い作るのは、

  • エラーの種類によって通知を分ける実装を挟みたい
  • エラーメッセージの中身をクライアントに見せたくない
  • その他エラーに関する独自ロジックを挟みたい

といったところだろうか。

これらは interceptor のような middleware で実装するイメージだが、ogen では middleware はexperimental だし、ogen が生成するコードが内部で response を書き込んでいるので、ogen の外側の middleware ではresponse の中身を調整する類の実装は難しそうだ。

実装

公式 doc にあるように、API スキーマに記述すべきことは以下だ。

  • components/schema に エラー型を定義
  • 全ての response に default response を追記し、同じ記述をする

コード生成後は Handler で error interface を満たすように実装すればよい。

例えばアプリケーション内部で発生するエラーを、サーバー起因のエラーとユーザー起因のエラーのどちらかに分類し、それをエラーメッセージに含めているとすれば、公式サイトの例について、以下のように実装できる。

type TrivialHandler struct{}

func (TrivialHandler) MeGet(ctx context.Context) (r *api.User, _ error) {
    me, err := doSomething()
    if err != nil {
        return nil, err
    }

    return &api.User{ID: me.ID}, nil
}

func (TrivialHandler) UsersPost(ctx context.Context, req *api.User) error {
    // implement
    return ht.ErrNotImplemented
}

func (TrivialHandler) NewError(ctx context.Context, err error) (r *api.ErrorStatusCode) {
    r = new(api.ErrorStatusCode)
    if strings.Contains(err.Error(), "[UserError]") {
        r = new(api.ErrorStatusCode)
        r.StatusCode = 400
        r.Response = api.Error{
            Code:    113355,
            Message: err.Error(),
        }
        return r
    }
    if strings.Contains(err.Error(), "[ServerError]") {
        fmt.Println("sending error to sentry...")
        // TODO: send error to sentry

        r = new(api.ErrorStatusCode)
        r.StatusCode = 500
        r.Response = api.Error{
            Code:    555000,
            Message: "please try again later",
        }
        return r
    }
    r.StatusCode = 500
    r.Response = api.Error{
        Code:    555000,
        Message: "unknown error",
    }
    return r
}

実際はカスタムエラーなどを定義して、型判定等をするのが良いと思う。

まとめ

ogen 独自の拡張として Convenient Error というものがあり、想定外の error をどう処理するか、API サーバー全体を横断して定義できる。

おまけの一言

ogen の開発が続いていくか心配です