LoginSignup
1
0

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

Last updated at Posted at 2023-12-23

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

皆様今年はどんな一年でしたか?
私は、あっという間の一年でした。気持ちはまだ1月です。
去年のアドベントカレンダーでたくさん書くって書きましたが、当記事が2023年初めての記事であり、最後の記事になりそうです。
本当にありがとうございます。

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

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

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

前提

  • OpenAPIでAPISpecを管理
  • OpenAPIGeneratorでコードを自動生成

oapi-codegenを読んでみる

マルチエラーに関するオプションやハンドラーを設定するところはあったので、そこを起点に諸々設定を追加していきました。

とりあえず、コード

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"
}

説明

重要なところだけ抜粋します。

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が出てきて、びっくりしました。

1
0
0

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
1
0