Refine ModelAttributeMethodProcessor Kotlin exception handling

This commit refines ModelAttributeMethodProcessor Kotlin exception
handling in order to throw a proper MethodArgumentNotValidException
instead of a NullPointerException when Kotlin null-safety constraints
are not fulfilled, translating to an HTTP error with 400 status code
(Bad Request) instead of 500 (Internal Server Error).

Closes gh-23846
This commit is contained in:
Sébastien Deleuze 2023-02-14 14:07:41 +01:00
parent 979118c1eb
commit 569df6eecc
3 changed files with 135 additions and 6 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.web.bind;
import java.lang.reflect.Executable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@ -42,13 +43,18 @@ import org.springframework.web.ErrorResponse;
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 3.1
*/
@SuppressWarnings("serial")
public class MethodArgumentNotValidException extends BindException implements ErrorResponse {
@Nullable
private final MethodParameter parameter;
@Nullable
private final Executable executable;
private final ProblemDetail body;
@ -60,6 +66,19 @@ public class MethodArgumentNotValidException extends BindException implements Er
public MethodArgumentNotValidException(MethodParameter parameter, BindingResult bindingResult) {
super(bindingResult);
this.parameter = parameter;
this.executable = null;
this.body = ProblemDetail.forStatusAndDetail(getStatusCode(), "Invalid request content.");
}
/**
* Constructor for {@link MethodArgumentNotValidException}.
* @param executable the executable that failed validation
* @param bindingResult the results of the validation
*/
public MethodArgumentNotValidException(Executable executable, BindingResult bindingResult) {
super(bindingResult);
this.parameter = null;
this.executable = executable;
this.body = ProblemDetail.forStatusAndDetail(getStatusCode(), "Invalid request content.");
}
@ -83,9 +102,16 @@ public class MethodArgumentNotValidException extends BindException implements Er
@Override
public String getMessage() {
StringBuilder sb = new StringBuilder("Validation failed for argument [")
.append(this.parameter.getParameterIndex()).append("] in ")
.append(this.parameter.getExecutable().toGenericString());
StringBuilder sb = new StringBuilder("Validation failed ");
if (this.parameter != null) {
sb.append("for argument [")
.append(this.parameter.getParameterIndex()).append("] in ")
.append(this.parameter.getExecutable().toGenericString());
}
else {
sb.append("in ")
.append(this.executable.toGenericString());
}
BindingResult bindingResult = getBindingResult();
if (bindingResult.getErrorCount() > 1) {
sb.append(" with ").append(bindingResult.getErrorCount()).append(" errors");

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -36,6 +36,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.TypeMismatchException;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@ -47,6 +48,7 @@ import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.ObjectError;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.ValidationAnnotationUtils;
@ -329,7 +331,21 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
throw new MethodArgumentNotValidException(parameter, result);
}
return BeanUtils.instantiateClass(ctor, args);
try {
return BeanUtils.instantiateClass(ctor, args);
}
catch (BeanInstantiationException ex) {
Throwable cause = ex.getCause();
if (KotlinDetector.isKotlinType(ctor.getDeclaringClass()) && cause instanceof NullPointerException) {
BindingResult result = binder.getBindingResult();
ObjectError error = new ObjectError(ctor.getName(), cause.getMessage());
result.addError(error);
throw new MethodArgumentNotValidException(ctor, result);
}
else {
throw ex;
}
}
}
/**

View File

@ -0,0 +1,87 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.method.annotation
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.*
import org.mockito.BDDMockito
import org.mockito.Mockito
import org.springframework.core.MethodParameter
import org.springframework.core.annotation.SynthesizingMethodParameter
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.bind.support.WebRequestDataBinder
import org.springframework.web.context.request.ServletWebRequest
import org.springframework.web.method.support.ModelAndViewContainer
import org.springframework.web.testfixture.servlet.MockHttpServletRequest
import kotlin.annotation.AnnotationTarget.*
/**
* Kotlin test fixture for [ModelAttributeMethodProcessor].
*
* @author Sebastien Deleuze
*/
class ModelAttributeMethodProcessorKotlinTests {
private lateinit var container: ModelAndViewContainer
private lateinit var processor: ModelAttributeMethodProcessor
private lateinit var param: MethodParameter
@BeforeEach
fun setup() {
container = ModelAndViewContainer()
processor = ModelAttributeMethodProcessor(false)
var method = ModelAttributeHandler::class.java.getDeclaredMethod("test",Param::class.java)
param = SynthesizingMethodParameter(method, 0)
}
@Test
fun resolveArgumentWithValue() {
val mockRequest = MockHttpServletRequest().apply { addParameter("a", "b") }
val requestWithParam = ServletWebRequest(mockRequest)
val factory = Mockito.mock<WebDataBinderFactory>()
BDDMockito.given(factory.createBinder(any(), any(), eq("param")))
.willAnswer { WebRequestDataBinder(it.getArgument(1)) }
Assertions.assertThat(processor.resolveArgument(this.param, container, requestWithParam, factory)).isEqualTo(Param("b"))
}
@Test
fun throwMethodArgumentNotValidExceptionWithNull() {
val mockRequest = MockHttpServletRequest().apply { addParameter("a", null) }
val requestWithParam = ServletWebRequest(mockRequest)
val factory = Mockito.mock<WebDataBinderFactory>()
BDDMockito.given(factory.createBinder(any(), any(), eq("param")))
.willAnswer { WebRequestDataBinder(it.getArgument(1)) }
Assertions.assertThatThrownBy {
processor.resolveArgument(this.param, container, requestWithParam, factory)
}.isInstanceOf(MethodArgumentNotValidException::class.java)
.hasMessageContaining("parameter a")
}
private data class Param(val a: String)
@Suppress("UNUSED_PARAMETER")
private class ModelAttributeHandler {
fun test(param: Param) { }
}
}