こんにちは!がちむちです!
皆様今年はどんな一年でしたか?
私は、あっという間の一年でした。気持ちはまだ1月です。
去年のアドベントカレンダーでたくさん書くって書きましたが、当記事が2023年初めての記事であり、最後の記事になりそうです。
本当にありがとうございます。
さて今年は表題の通りで、窓際でGolangを使ったアプリケーションを結構書いていたりしました。
ちなみに、タイトルですが、よく勘違いされますが、窓際「で」なので。
「へ」ではないです。すでに窓際です。
久々にAPIのサーバーアプリケーションを作ったのですが、フロントチームとエラー周りの仕様をつめているときに「APIのバリデーションでエラーが複数あったら、いい感じに複数返してほしい」と言われました。
元々JavaのSpring界隈勢だったので、当然できると思っていたので、二つ返事でOKとしてしまいましたが、初夏の恐怖体験で軽めの地獄の一丁目だったので、軌跡を残しておこうと思います。
前提
- OpenAPIでAPISpecを管理
- OpenAPIGeneratorでコードを自動生成
- 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
とりあえず、コード
% 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"
}
説明
重要なところだけ抜粋します。
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
}
}
...
以下でエラー処理の設定を行っていています。
options := mw.Options{
Options: openapi3filter.Options{
MultiError: true
},
ErrorHandler: errorHandler(),
MultiErrorHandler: multiErrorHandler(),
}
マルチエラーを有効化して、マルチエラーのハンドラーとエラーのハンドラーをセットします。
呼び出される順番は、マルチエラーハンドラー→エラーハンドラーの順番になります。
今回一番困ったのは、マルチエラーでいい感じにエラーをまとめて、errorを返すのですが、最後に呼ばれるエラーハンドラーの関数のシグネチャーが↓のようになっています。
// ErrorHandler is called when there is an error in validation
type ErrorHandler func(w http.ResponseWriter, message string, statusCode int)
実際に呼び出されるときは以下のようになっています。
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)
})
}
つまり、エラーメッセージが渡ってくるという状況で、errorが渡ってこないのです。\(^o^)/
なので、自前のmultierrorのハンドラーで返すエラーのメッセージに無理やりjsonを埋め込みながら、マルチエラーか判断できるようにしておいて、errorhandlerでマルチエラーかどうかを判断してerrorのオブジェクトに入れて返すようにしました。
流石にこれをチームにPRで出したときは、( ゚д゚)ポカーンってされたので、出るところ出てやりました。
本家にPRだしっった
同じような問題で困っている人がいるかわからないですが、エラーハンドリングと言いつつメッセージが渡ってくるというハートウォーミングな展開は、流石に窓際の心臓にも良くないので、PR送ってみました。
瞬間、PR、重ねて
自分がPR出したあとに、1時間ぐらいの勢いで全く同じ内容(実装しているフレームワークが違うが)のPRが出てきて、びっくりしました。