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

View File

@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.validation.BindException import org.springframework.validation.BindException
import org.springframework.validation.FieldError
import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.ExceptionHandler
@ -52,23 +53,43 @@ class MastodonApiControllerAdvice {
@ExceptionHandler(BindException::class) @ExceptionHandler(BindException::class)
fun handleException(ex: BindException): ResponseEntity<UnprocessableEntityResponse> { fun handleException(ex: BindException): ResponseEntity<UnprocessableEntityResponse> {
logger.debug("Failed bind entity.", ex) 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 { val details = mutableMapOf<String, MutableList<UnprocessableEntityResponseDetails>>()
it.field to UnprocessableEntityResponseDetails(
when (it.code) { ex.allErrors.forEach {
"Email" -> "ERR_INVALID" val defaultMessage = it.defaultMessage
"Pattern" -> "ERR_INVALID" when {
else -> "ERR_INVALID" it is FieldError -> {
}, val code = when (it.code) {
it.defaultMessage "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) @ExceptionHandler(StatusNotFoundException::class)

View File

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

View File

@ -36,3 +36,4 @@ Not Integer, not Long => we have a decimal value!
}}{{^isInteger}}{{^isLong}}{{#minimum}} }}{{^isInteger}}{{^isLong}}{{#minimum}}
@get:DecimalMin("{{.}}"){{/minimum}}{{#maximum}} @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}} {{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} @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}} @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}}