窓際でGo! 〜瞬間、PR、重ねて〜

こんにちは！がちむちです！

皆様今年はどんな一年でしたか？
私は、あっという間の一年でした。気持ちはまだ１月です。
[去年のアドベントカレンダーでたくさん書く](https://qiita.com/gachi-muchi-engineer/items/0880535896dccce4f483#%E3%81%BE%E3%81%A8%E3%82%81)って書きましたが、当記事が2023年初めての記事であり、最後の記事になりそうです。
本当にありがとうございます。

さて今年は表題の通りで、窓際でGolangを使ったアプリケーションを結構書いていたりしました。
ちなみに、タイトルですが、よく勘違いされますが、窓際「で」なので。
「へ」ではないです。すでに窓際です。

久々にAPIのサーバーアプリケーションを作ったのですが、フロントチームとエラー周りの仕様をつめているときに「APIのバリデーションでエラーが複数あったら、いい感じに複数返してほしい」と言われました。

元々JavaのSpring界隈勢だったので、当然できると思っていたので、二つ返事でOKとしてしまいましたが、初夏の恐怖体験で軽めの地獄の一丁目だったので、軌跡を残しておこうと思います。

# 前提
* OpenAPIでAPISpecを管理
* OpenAPIGeneratorでコードを自動生成
    * [oapi-codegen](https://github.com/deepmap/oapi-codegen)を利用

# oapi-codegenを読んでみる
マルチエラーに関するオプションやハンドラーを設定するところはあったので、そこを起点に諸々設定を追加していきました。
* https://github.com/getkin/kin-openapi/blob/62bf0f764655ea170bf8ccc32fba323598123d0e/openapi3filter/options.go#L26
* https://github.com/oapi-codegen/nethttp-middleware/blob/main/oapi_validate.go#L29

# とりあえず、コード
[sample-multi-error](https://github.com/hnarimiya/sample-multi-error)
```
% go run main.go
....
% curl -s -X POST http://localhost:8080/contents -H "content-type: application/json" -d {} | jq
{
  "errors": [
    {
      "error": "property \"name\" is missing",
      "field": "name"
    },
    {
      "error": "property \"description\" is missing",
      "field": "description"
    }
  ],
  "message": "validation error"
}

```

# 説明
重要なところだけ抜粋します。
```golang
package main

....

func main() {
...
	options := mw.Options{
		Options: openapi3filter.Options{
			MultiError: true
		},
		ErrorHandler:      errorHandler(),
		MultiErrorHandler: multiErrorHandler(),
	}

...
}

....
func errorHandler() func(w http.ResponseWriter, message string, statusCode int) {
	return func(w http.ResponseWriter, message string, statusCode int) {
		var body server.ErrorResponse
		if strings.Index(message, "multi_error:") == 0 {
			_ = json.Unmarshal([]byte(strings.SplitN(message, ":", 2)[1]), &body)
		} else {
			body.Message = message
		}
		writeJSON(w, statusCode, body)
	}
}

....
func multiErrorHandler() func(me openapi3.MultiError) (int, error) {
	return func(me openapi3.MultiError) (int, error) {
...

		var fieldErrors []FieldError
		for _, err := range errorList {
			var e *openapi3.SchemaError
			if errors.As(err, &e) {
				fieldErrors = append(fieldErrors, FieldError{
					Field: convertJSONPath(e.JSONPointer()),
					Error: e.Reason,
				})
			}
		}

		validationError := ValidationError{
			Message: "validation error",
			Errors:  fieldErrors,
		}
		return http.StatusBadRequest, validationError
	}
}
...
```
以下でエラー処理の設定を行っていています。
```golang
	options := mw.Options{
		Options: openapi3filter.Options{
			MultiError: true
		},
		ErrorHandler:      errorHandler(),
		MultiErrorHandler: multiErrorHandler(),
	}
```
マルチエラーを有効化して、マルチエラーのハンドラーとエラーのハンドラーをセットします。
呼び出される順番は、マルチエラーハンドラー→エラーハンドラーの順番になります。

今回一番困ったのは、マルチエラーでいい感じにエラーをまとめて、errorを返すのですが、最後に呼ばれるエラーハンドラーの関数のシグネチャーが↓のようになっています。
```golang
// ErrorHandler is called when there is an error in validation
type ErrorHandler func(w http.ResponseWriter, message string, statusCode int)
```
https://github.com/oapi-codegen/nethttp-middleware/blob/main/oapi_validate.go#L20

実際に呼び出されるときは以下のようになっています。
```golang
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

			// validate request
			if statusCode, err := validateRequest(r, router, options); err != nil {
				if options != nil && options.ErrorHandler != nil {
					options.ErrorHandler(w, err.Error(), statusCode)
				} else {
					http.Error(w, err.Error(), statusCode)
				}
				return
			}

			// serve
			next.ServeHTTP(w, r)
		})
	}
```
https://github.com/oapi-codegen/nethttp-middleware/blob/main/oapi_validate.go#L56

つまり、エラーメッセージが渡ってくるという状況で、errorが渡ってこないのです。＼(^o^)／
なので、自前のmultierrorのハンドラーで返すエラーのメッセージに無理やりjsonを埋め込みながら、マルチエラーか判断できるようにしておいて、errorhandlerでマルチエラーかどうかを判断してerrorのオブジェクトに入れて返すようにしました。

流石にこれをチームにPRで出したときは、( ﾟдﾟ)ﾎﾟｶｰﾝってされたので、出るところ出てやりました。

# 本家にPRだしっった
https://github.com/deepmap/oapi-codegen/pull/1143

同じような問題で困っている人がいるかわからないですが、エラーハンドリングと言いつつメッセージが渡ってくるというハートウォーミングな展開は、流石に窓際の心臓にも良くないので、PR送ってみました。

# 瞬間、PR、重ねて
自分がPR出したあとに、1時間ぐらいの勢いで全く同じ内容(実装しているフレームワークが違うが)のPRが出てきて、びっくりしました。

https://github.com/deepmap/oapi-codegen/pull/1145