sunabak0 / realworld-kotlin-springboot-jdbc

勉強:Kotlin/SpringBoot/アプリアーキテクチャ/戦術的DDD/エラーハンドリング/ソフトウェアテスト/OpenAPIからコードの自動生成
https://sunabak0.github.io/realworld-kotlin-springboot-jdbc/
4 stars 0 forks source link

SpringBootの異常系(Exception)をキャッチしてハンドリングするための調査 #105

Closed sunakan closed 2 years ago

sunakan commented 2 years ago

現在

解決/達成したいこと

改めて考えたいこと

参考

sunakan commented 2 years ago

キーワードメモ

@RestControllerAdvice

@ExceptionHandler

カスタム例外ハンドラー

Adviceとは差し込みたい処理

sunakan commented 2 years ago

RFC 7807 Problem Details for HTTP APIs

RFC 7807 - Problem Details for HTTP APIs 日本語訳 https://tex2e.github.io/rfc-translater/html/rfc7807.html

sunakan commented 2 years ago
   HTTP/1.1 400 Bad Request
   Content-Type: application/problem+json
   Content-Language: en

   {
   "type": "https://example.net/validation-error",
   "title": "Your request parameters didn't validate.",
   "invalid-params": [ {
                         "name": "age",
                         "reason": "must be a positive integer"
                       },
                       {
                         "name": "color",
                         "reason": "must be 'green', 'red' or 'blue'"}
                     ]
   }
sunakan commented 2 years ago
   HTTP/1.1 403 Forbidden
   Content-Type: application/problem+json
   Content-Language: en

   {
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    "balance": 30,
    "accounts": ["/account/12345",
                 "/account/67890"]
   }
sunakan commented 2 years ago

素案

@RestController
class UserAndAuthenticationController(
    val mySessionJwt: MySessionJwt,
    val myAuth: MyAuth,
    val registerUserUseCase: RegisterUserUseCase,
    val loginUseCase: LoginUseCase,
    val updateUserUseCase: UpdateUserUseCase,
) : UsersApi {

    override fun createUser(body: NewUserRequest): ResponseEntity<UserResponse> {
        val registeredUser = registerUserUseCase.execute(body.user.email, body.user.email, body.user.password).fold(
            { throw CreateUserUseCaseErrorException(it) },
            { it }
        )
        val token: String = mySessionJwt.encode(MySession(registeredUser.userId, registeredUser.email)).fold(
            { throw RealworldSessionEncodeErrorException(it) },
            { it }
        )
        return ResponseEntity(
            UserResponse(
                user = User(
                    email = registeredUser.email.value,
                    username = registeredUser.username.value,
                    bio = registeredUser.bio.value,
                    image = registeredUser.image.value,
                    token = token
                )
            ),
            HttpStatus.valueOf(201)
        )
    }

    data class CreateUserUseCaseErrorException(val error: RegisterUserUseCase.Error) : Exception()

    @ExceptionHandler(value = [CreateUserUseCaseErrorException::class])
    fun onCreateUserUseCaseErrorException(e: CreateUserUseCaseErrorException): ResponseEntity<GenericErrorModel> {
        val generateResponseEntity: (List<String>) -> ResponseEntity<GenericErrorModel> = { body ->
            ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = body)),
                HttpStatus.valueOf(422)
            )
        }
        return when (val error = e.error) {
            is RegisterUserUseCase.Error.AlreadyRegisteredEmail -> generateResponseEntity(listOf("メールアドレスはすでに登録されています"))
            is RegisterUserUseCase.Error.AlreadyRegisteredUsername -> generateResponseEntity(listOf("ユーザー名は既に登録されています"))
            is RegisterUserUseCase.Error.InvalidUser -> generateResponseEntity(error.errors.map { it.message })
            is RegisterUserUseCase.Error.Unexpected -> throw NotImplementedError()
        }
    }
}
sunakan commented 2 years ago

コントローラ内にExceptionHandler書くのどうなん?優先度的に。。とか考えてたら、コントローラでのハンドリングを勧めている記事発見

【Spring】共通的かつ特別なハンドリングを行いたかった

各コントローラーのExceptionHandler -> RestControllerAdvices(優先度順) です。 また、各コントローラーのExceptionHandlerでキャッチした後、適当にExceptionをthrowsしても共通的なRestControllerAdvicesに遷移しませんでした。 なので、各コントローラー内でハンドリングするのであれば、キャッチした後にきっちり例外が起こらないようにする必要があります。

おぉ!自分が知りたかったことを書いてある!! APIテストでも後々確認できるけど、そのまま(コントローラ内にハンドラ書く)で行けそう

sunakan commented 2 years ago

ハンドリングできたので閉じます