feat: Null非許容のパラメーターにnullがきたときのエラーメッセージを改善

This commit is contained in:
usbharu 2024-02-24 13:53:48 +09:00
parent b33f62b656
commit 5c8d7e8d36
5 changed files with 52 additions and 21 deletions

View File

@ -141,10 +141,11 @@ class AccountApiTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-3")
param("password", "api-test-user-3")
with(csrf())
}
.andExpect { status { isBadRequest() } }
.andDo { print() }
.andExpect { status { isUnprocessableEntity() } }
}
@Test
@ -156,7 +157,7 @@ class AccountApiTest {
param("username", "api-test-user-4")
with(csrf())
}
.andExpect { status { isBadRequest() } }
.andExpect { status { isUnprocessableEntity() } }
}
@Test

View File

@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.validation.BindException
import org.springframework.validation.FieldError
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
@ -52,23 +53,43 @@ class MastodonApiControllerAdvice {
@ExceptionHandler(BindException::class)
fun handleException(ex: BindException): ResponseEntity<UnprocessableEntityResponse> {
logger.debug("Failed bind entity.", ex)
val error = ex.bindingResult.fieldErrors
val message = error.map {
"${it.field} ${it.defaultMessage}"
}.joinToString(prefix = "Validation failed: ")
val details = error.associate {
it.field to UnprocessableEntityResponseDetails(
when (it.code) {
"Email" -> "ERR_INVALID"
"Pattern" -> "ERR_INVALID"
else -> "ERR_INVALID"
},
it.defaultMessage
)
val details = mutableMapOf<String, MutableList<UnprocessableEntityResponseDetails>>()
ex.allErrors.forEach {
val defaultMessage = it.defaultMessage
when {
it is FieldError -> {
val code = when (it.code) {
"Email" -> "ERR_INVALID"
"Pattern" -> "ERR_INVALID"
else -> "ERR_INVALID"
}
details.getOrPut(it.field) {
mutableListOf()
}.add(UnprocessableEntityResponseDetails(code, defaultMessage.orEmpty()))
}
defaultMessage?.startsWith("Parameter specified as non-null is null:") == true -> {
val parameter = defaultMessage.substringAfterLast("parameter ")
details.getOrPut(parameter) {
mutableListOf()
}.add(UnprocessableEntityResponseDetails("ERR_BLANK", "can't be blank"))
}
else -> {
logger.warn("Unknown validation error", ex)
}
}
}
return ResponseEntity.unprocessableEntity().body(UnprocessableEntityResponse(message, details))
val message = details.map {
it.key + " " + it.value.joinToString { it.description }
}.joinToString()
return ResponseEntity.unprocessableEntity()
.body(UnprocessableEntityResponse(message, details))
}
@ExceptionHandler(StatusNotFoundException::class)

View File

@ -1455,6 +1455,8 @@ components:
type: object
properties:
username:
nullable: false
type: string
minLength: 1
maxLength: 300
@ -2841,9 +2843,11 @@ components:
error:
type: string
details:
type: object
type: array
additionalProperties:
$ref: "#/components/schemas/UnprocessableEntityResponseDetails"
type: array
items:
$ref: "#/components/schemas/UnprocessableEntityResponseDetails"
UnprocessableEntityResponseDetails:
type: object
@ -2852,6 +2856,10 @@ components:
type: string
description:
type: string
nullable: false
required:
- error
- description
UnauthorizedResponse:
type: object

View File

@ -35,4 +35,5 @@ isLong set
Not Integer, not Long => we have a decimal value!
}}{{^isInteger}}{{^isLong}}{{#minimum}}
@get:DecimalMin("{{.}}"){{/minimum}}{{#maximum}}
@get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}}
@get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}}

View File

@ -1,4 +1,4 @@
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{^isNullable}}@get:NotNull{{/isNullable}}
@get:JsonProperty("{{{baseName}}}", required = true){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}