🐁

Go の ogen で Convenient errors を使う

2024/06/18に公開

はじめに

ogen の自動生成を使っていてずっと気になっていたログ。

INFO    convenient      Convenient errors are not available

INFO レベルだしまあいいかと放置していましたが、ようやくこれが何か知りました。

https://ogen.dev/docs/concepts/convenient_errors

この Convenient errors を使えば ogen でのエラーハンドリングを共通化することができます。
ogen のエラーハンドリングってなんかイケテナイんだよなあと思っていたことも同時に解消しました。
この記事を通して少しでもみなさんの ogen ライフが快適になれば幸いです。

ogen

https://ogen.dev/

OpenAPI からコードを自動生成することができ、以下のような特徴があります。

  • 生成されたコードで refrect や interface{} を使わない
  • 独自のルーティング機構
  • 標準の OpenAPI v3 で完結
  • Optional や Nullable に対応
  • OpenTelemetry に対応

以前私が oapi-codegen と比較した記事もあるのでそちらもご覧ください。

https://zenn.dev/otakakot/articles/43653194611d42

事前準備

バージョン

go version
go version go1.22.4 darwin/arm64
ogen version

v1.2.1
※ 手元にインストールせずに利用するので実行時のバージョンタグを記載

openapi.yaml

コード自動生成に利用する OpenAPI は以下のようなものとなります。

openapi.yaml
openapi: 3.0.3
info:
  title: Sample Go ogen Convenient errors
  description: |-
    This is the Sample Go ogen APP API documentation.
  termsOfService: https://localhost:8080
  contact:
    email: kotaro.otaka@example.com
  license:
    name: Apache 2.0
  version: 0.0.1
externalDocs:
  description: Find out more about Swagger
  url: http://swagger.io
servers:
  - url: http://localhost:8080
tags:
  - name: Sample
    description: Sample
paths:
  /:
    get:
      tags:
        - Sample
      summary: Sample
      description: Sample
      operationId: getSample
      parameters:
        - name: status
          in: query
          description: status
          required: true
          schema:
            type: integer
            example: 200
      responses:
        "200":
          description: OK
        "400":
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not Found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags:
        - Sample
      summary: Sample
      description: Sample
      operationId: postSampl
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: integer
                  description: status
                  example: 200
              required:
                - status
      responses:
        "200":
          description: OK
        "400":
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not Found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  schemas:
    Error:
      type: object
      properties:
        message:
          type: string
      required:
        - message

サンプルコードのため以下のような API 設計としています。

  • リクエストにてレスポンスのステータスコードを指定
  • GET と POST で機能を提供

Convenient errors なしの実装

ogen はレスポンスの型を強力にサポートしてくれるので各APIごとにステータスコードそれぞれ型を提供してくれます。

https://github.com/otakakot/sample-go-ogen-convenient-errors/blob/main/cmd/off/main.go#L16-L70

そのためこのように丁寧にレスポンスを実装してあげる必要があります。
各 API ごとにレスポンスの分岐コードが発生するのでかなり冗長です。

また、処理の結果はエラーなのにメソッドの返り値ではエラーを返さない( 第二戻り値が nil ) となるので Go っぽいコードではなくなります。

※ WithErrorHandler() を使って感じの共通化したエラーハンドラーがかけるかもしれないです。ジェネリクスとか使って。知っている方いらっしゃいましたら教えてください。

https://github.com/ogen-go/ogen/blob/a74092b636e6f50a166ff16850c540146a792c89/gen/_template/cfg.tmpl#L267-L274

Convenient errors ありの実装

Convenient erros を利用するために openapi.yaml を以下のように書き換えます。
ステータスコード 500 で定義していたものを default にします。

-        "500":
+        default:
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

この変更をすべてのAPIに対して行います。
また、この default で参照する定義はすべて同じものである必要があります。

公式サイトの記載
  • Every operation defines the same default response
  • This response defines only one application/json media

openapi.yaml を修正することによって自動生成コードに変化が現れます。

  • interface として NewError() メソッドが必要となる
  • エラーハンドリング部分に下記実装が追加される
    +if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil {
    +  defer recordError("Internal", err)
    +}
    

また、コード自動生成実行時のログも変化し以下のような出力となります。

INFO    convenient      Generating convenient error response

この実装を使うことで以下のようにエラーハンドリングを共通化して実装することができます。

https://github.com/otakakot/sample-go-ogen-convenient-errors/blob/main/cmd/on/main.go#L18-L102

Convinient errors なしの場合よりコードの量が多くなっていますが、 API の数が多くなればなるほど共通化の恩恵を受けることができるでしょう。
ただし、デメリットとして openapi.yaml で定義していないステータスコードも返る可能性がでてきます。
その点は気をつけていきたいです。

おわりに

Convenient errors を使うと ogen その名の通り便利になります。
ogen のエラーハンドリングで冗長なコードを書いているなと思ったらぜひ使ってみてください。

今回実装したコードは以下に置いておきます。

https://github.com/otakakot/sample-go-ogen-convenient-errors

Discussion