failed to validate request params in a Spring Boot/Kotlin Coroutines controller - spring-boot

In a SpringBoot/Kotlin Coroutines project, I have a controller class like this.
#RestContollser
#Validated
class PostController(private val posts: PostRepository) {
suspend fun search(#RequestParam q:String, #RequestParam #Min(0) offset:Int, #RequestParam #Min(1) limit:Int): ResponseEntity<Any> {}
}
The validation on the #ResquestBody works as the general Spring WebFlux, but when testing
validating request params , it failed and throws an exception like:
java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below:
It is not a ConstraintViolationException.

I think this is a bug in the framework when you are using coroutines (update , it is, I saw Happy Songs comment). In summary:
"#Validated is indeed not yet Coroutines compliant, we need to fix that by using Coroutines aware methods to discover method parameters."
The trouble is that the signature of the method on your controller is actually enhanced by Spring to have an extra parameter, like this, adding a continuation:
public java.lang.Object com.example.react.PostController.search(java.lang.String,int,int,kotlin.coroutines.Continuation)
so when the hibernate validator calls getParameter names to get the list of parameters on your method, it thinks there are 4 in total on the request, and then gets an index out of bounds exception trying to get the 4th (index 3).
If you put a breakpoint on the return of this:
#Override
public E get(int index) {
return a[index];
}
and put a breakpoint condition of index ==3 && a.length <4 you can see what is going on.
I'd report it as a bug on the Spring issue tracker.
You might be better off taking an alternative approach, as described here, using a RequestBody as a DTO and using the #Valid annotation
https://www.vinsguru.com/spring-webflux-validation/

Thanks for the happy songs' comments, I found the best solution by now to overcome this barrier from the Spring Github issues#23499.
As explained in comments of this issue and PaulNuk's answer, there is a Continuation will be appended to the method arguments at runtime, which will fail the index computation of the method parameter names in the Hibernate Validator.
The solution is changing the ParameterNameDiscoverer.getParameterNames(Method) method and adding a empty string as the additional parameter name when it is a suspend function.
class KotlinCoroutinesLocalValidatorFactoryBean : LocalValidatorFactoryBean() {
override fun getClockProvider(): ClockProvider = DefaultClockProvider.INSTANCE
override fun postProcessConfiguration(configuration: javax.validation.Configuration<*>) {
super.postProcessConfiguration(configuration)
val discoverer = PrioritizedParameterNameDiscoverer()
discoverer.addDiscoverer(SuspendAwareKotlinParameterNameDiscoverer())
discoverer.addDiscoverer(StandardReflectionParameterNameDiscoverer())
discoverer.addDiscoverer(LocalVariableTableParameterNameDiscoverer())
val defaultProvider = configuration.defaultParameterNameProvider
configuration.parameterNameProvider(object : ParameterNameProvider {
override fun getParameterNames(constructor: Constructor<*>): List<String> {
val paramNames: Array<String>? = discoverer.getParameterNames(constructor)
return paramNames?.toList() ?: defaultProvider.getParameterNames(constructor)
}
override fun getParameterNames(method: Method): List<String> {
val paramNames: Array<String>? = discoverer.getParameterNames(method)
return paramNames?.toList() ?: defaultProvider.getParameterNames(method)
}
})
}
}
class SuspendAwareKotlinParameterNameDiscoverer : ParameterNameDiscoverer {
private val defaultProvider = KotlinReflectionParameterNameDiscoverer()
override fun getParameterNames(constructor: Constructor<*>): Array<String>? =
defaultProvider.getParameterNames(constructor)
override fun getParameterNames(method: Method): Array<String>? {
val defaultNames = defaultProvider.getParameterNames(method) ?: return null
val function = method.kotlinFunction
return if (function != null && function.isSuspend) {
defaultNames + ""
} else defaultNames
}
}
Then declare a new validator factory bean.
#Primary
#Bean
#Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun defaultValidator(): LocalValidatorFactoryBean {
val factoryBean = KotlinCoroutinesLocalValidatorFactoryBean()
factoryBean.messageInterpolator = MessageInterpolatorFactory().getObject()
return factoryBean
}
Get the complete sample codes from my Github.

Related

How to test findById method?

First - I've checked all previous topics around this question and none of them helped.
Having the following code:
#DisplayName("GET RecipeUltraLight by id is successful")
#Test
public void givenRecipeId_whenGetRecipeDetailsById_thenReturnRecipeObject(){
// given
given(this.recipeRepository.findById(recipe.getId())).willReturn(Optional.of(recipe));
given(this.recipeService.getRecipeById(recipe.getId())).willReturn(recipe);
given(this.recipeConverter.toUltraLight(recipe)).willReturn(recipeUltraLightDto);
// when
RecipeUltraLightDto retrievedRecipe = recipeService.getRecipeUltraLightById(recipe.getId());
// then
verify(recipeRepository, times(1)).findById(recipe.getId());
verify(recipeService, times(1)).getRecipeById(recipe.getId());
verify(recipeConverter, times(1)).toUltraLight(recipe);
assertThat(retrievedRecipe).isNotNull();
}
gives me this error:
org.mockito.exceptions.misusing.WrongTypeOfReturnValue:
Recipe cannot be returned by findById()
findById() should return Optional
***
If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
1. This exception *might* occur in wrongly written multi-threaded tests.
Please refer to Mockito FAQ on limitations of concurrency testing.
2. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies -
- with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.
Service method:
#Transactional(readOnly = true)
public RecipeUltraLightDto getRecipeUltraLightById(Long id) {
Recipe recipe = getRecipeById(id);
RecipeUltraLightDto dto = new RecipeUltraLightDto();
dto = recipeConverter.toUltraLight(recipe);
return dto;
}
// internal use only
#Transactional(readOnly = true)
public Recipe getRecipeById(Long id) {
if (id == null || id < 1) {
return null;
}
return recipeRepository.findById(id)
.orElseThrow(() -> new RecipeNotFoundException(
String.format("Recipe with id %d not found.", id)
));
}
Setup:
#ContextConfiguration(classes = {RecipeService.class})
#ExtendWith({SpringExtension.class, MockitoExtension.class})
class RecipeServiceTest {
#MockBean
private RecipeConverter recipeConverter;
#MockBean
private RecipeRepository recipeRepository;
#Autowired
private RecipeService recipeService;
private Recipe recipe;
private RecipeUltraLightDto recipeUltraLightDto;
#BeforeEach
public void setup(){
recipe = Recipe.builder()
.id(1L)
.name("Recipe")
.description("Description")
.createdAt(LocalDateTime.now())
.difficulty(RecipeDifficulty.EASY)
.minutesRequired(60)
.portions(4)
.authorId(1L)
.views(0)
.isVerified(false)
.build();
recipeUltraLightDto = RecipeUltraLightDto.builder()
.id(1L)
.name("Recipe")
.build();
}
I've tried:
Optinal.ofNullable()
Adding .isPresent()
Getting rid of .orElseThrow and going through if statements and using .get()
Kotlin
Will be glad if someone can help.
You are creating a mock of the object you are testing and with that basically also render the mocking of the repository useless.
You should remove the line given(this.recipeService.getRecipeById(recipe.getId())).willReturn(recipe); that way it will just call the method and call the repository. Which now will return the mocked result. As that is the behavior that will now kick in.
It is clearly mentioned that the method findById() returning Optional, you need to get Recipe by invoking Optional.get().

Annotation AliasFor suddenly stopped working

I am about to have nervous break down. Things just randomly stops working for no apparent reason.
The question I am asking is have someone encountered similar problems or possibly could help me with my specific problem.
I have basic Spring Boot web application and I have defined end points using GetMapping and Postmapping annotations and I introduced my own annotation. Things worked fine and when looking at GetMapping and PostMapping source they clearly maps some attributes for RequesyMapping using the AliasFor annotation. I am working on code that scan all Controller annotated beans and scan their methods for PermissionRequired annotation. When on is found it tries to find RequestMapping annotation and get the path(s) from it. The path(s) are empty and only the method type from GetMapping is present. It all worked fine for sometime, but then I typed more code and it stopped working, I am just wondering why.
Here are some code examples:
The method that tries to scan method with specific annotation:
#Bean
public Set<Pair<String, String>> requiredPermissions(ApplicationContext ac)
{
LOG.trace("Configuring required permissions");
if (ac != null)
{
for (String bean : ac.getBeanNamesForAnnotation(Controller.class))
{
for (Method m : ac.getAutowireCapableBeanFactory().getType(bean).getDeclaredMethods())
{
RequiresPermission rp = m.getAnnotation(RequiresPermission.class);
if (rp != null)
{
RequestMapping rm = AnnotationUtils.getAnnotation(m, RequestMapping.class);
if (rm != null)
{
LOG.trace("=============================================== Bean method {}[{}] annotation: {}", rm.path(), rp.value(), rm);
}
}
}
}
}
Set<Pair<String, String>> req = new HashSet<>();
req.add(Pair.of("/login", "GET"));
req.add(Pair.of("/login", "POST"));
// TODO: Scan methods
Method[] mtds = {};
for (Method m : mtds)
{
RequiresPermission rp = m.getAnnotation(RequiresPermission.class);
if (rp != null)
{
RequestMapping rm = m.getAnnotation(RequestMapping.class);
if (rm != null)
{
for (RequestMethod remet : rm.method())
{
for (String path : rm.path())
{
Pair<String, String> p = Pair.of(path, remet.toString().toUpperCase());
LOG.trace("Found URI requiring permission {}", p);
req.add(p);
}
}
}
}
}
return req;
}
The custom annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface RequiresPermission
{
String value() default "";
}
The controller class:
#Controller
public class PermissionedController
{
#GetMapping("/permmisioned/{param}")
#RequiresPermission("perm1:perm2")
protected String permissioned()
{
return "";
}
Yes there are typos and lines missing etc. But I am too tired (fatiqued) to fix those issues. This is quite simple and it should basically work, but it doesnt. Sorry, but no gist maybe someday if I am feeling better, but I doubt it. I could do with a vacation, but you do not get vacation from neurological disease.

spring reactive get locale in repository #Query

I have localized entities in a reactive spring boot application.
The current implementation works fine by depending on the Accept-Language header:
Controller
#RestController
class TaskController(val taskService: TaskService) {
#GetMapping("/tasks")
suspend fun getTasks(locale: Locale): Map<Long?,Task> {
return taskService.getTasks(locale).toList().associateBy(Task::id)
}
}
Service
#Service
class TaskServiceImpl(val taskRepository : TaskRepository) : TaskService {
override fun getTasks(locale: Locale): Flow<Task> {
return taskRepository.findWithLocale(locale.language)
}
Repository
interface TaskRepository : CoroutineCrudRepository<Task, Long> {
#Query("SELECT * FROM task t LEFT JOIN task_l10n tl10n ON tl10n.task_id=t.id WHERE tl10n.locale = :locale")
fun findWithLocale(locale: String): Flow<Task>
}
Question
As you can see, I need to pass down the locale from the controller to the service and then to the repository. Because a lot of our API will suffer from this noise/boilerplate I wonder if I somehow can use/inject the locale.language in a #Query("... where locale = :locale") without passing it as a parameter?
In MVC we could use LocaleContextHolder.getLocale() at least on the service level and get rid of some of the noise but it is not available in a reactive stack because it is bound to the thread and not to the coroutine.
APPROACH
From this spring.io post I hope to be able to setup the SpEL evaluation context in a request-aware way - i.e. to access the locale of the current request.
Something along the lines of this
#Query("... WHERE tl10n.locale = ?#{locale().language}"
But I cannot figure out these things:
how to put the reactive context on the SpEL context on a per request basis?
How to populate the reactive context with the request's locale?
Step 1
First I found that the exchange would provide access to the locale by debugging how Spring resolves the controller method's argument: it's done in ServerWebExchangeMethodArgumentResolver
exchange.getLocaleContext().getLocale()
So if I could access the exchange from a SpEL context, I would be happy.
?#{exchange.getLocaleContext().getLocale()}
which is not possible, because the SpEL context does not know about the reactive context:
Property or field 'exchange' cannot be found on object of type 'java.lang.Object[]' - maybe not public or not valid?
Step 2
Next I figured out that I can come up with a SpEL extension holding the locale and add the extension to the reactive repositories (actually I am using kotlin coroutines based CoroutineCrudRepository) by using a BeanPostProcessor on my repositories to make my custom SpEL extension available in the #Query("... ?#{locale()}") methods:
/**
* Adds extensions to the SpEL evaluation context.
*/
#Configuration
class RepositorySpELExtensionConfiguration {
companion object {
// list of provided extensions
val contextProviderWithExtensions =
ReactiveExtensionAwareQueryMethodEvaluationContextProvider(listOf(ReactiveLocaleAwareSpELExtension.INSTANCE))
}
/**
* Registers the customizer to the context to make spring aware of the bean post processor.
*/
#Bean
fun spELContextInRepositoriesCustomizer(): AddExtensionsToRepositoryBeanPostProcessor {
return AddExtensionsToRepositoryBeanPostProcessor()
}
/**
* Sets the [contextProviderWithExtensions] for SpEL in the [R2dbcRepositoryFactoryBean]s which makes the extensions
* usable in `#Query(...)` methods.
*/
class AddExtensionsToRepositoryBeanPostProcessor : BeanPostProcessor {
override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any {
if (bean is R2dbcRepositoryFactoryBean<*, *, *>) {
bean.addRepositoryFactoryCustomizer { it.setEvaluationContextProvider(contextProviderWithExtensions) }
}
return bean
}
}
/**
* Makes the [LocaleAwareSpELExtension] available in a reactive context.
*/
enum class ReactiveLocaleAwareSpELExtension : ReactiveEvaluationContextExtension {
INSTANCE;
override fun getExtension(): Mono<out EvaluationContextExtension> {
return Mono.just(LocaleAwareSpELExtension("en"))
}
override fun getExtensionId(): String {
ReactiveQueryMethodEvaluationContextProvider.DEFAULT
return "localeAwareSpELExtension"
}
}
/**
* Provides the requests locale as SpEL extension.
*
* Use it like this:
* ```
* #Query("... WHERE locale = :#{locale()}")
* ```
*/
class LocaleAwareSpELExtension(private val locale: String) : EvaluationContextExtension {
override fun getRootObject(): LocaleAwareSpELExtension {
return this
}
override fun getExtensionId(): String {
return "localeAwareSpELExtension"
}
#Suppress("unused") // Potentially used by `#Query(...) methods.
fun locale(): String {
return locale
}
}
}
This works fine hurray - but as you can see, I hardcoded the locale to "en" when creating the extension.
override fun getExtension(): Mono<out EvaluationContextExtension> {
return Mono.just(LocaleAwareSpELExtension("en"))
}
When debugging this line, I know that it is executed on a per request basis, which makes me hope that I should be able to somehow use the request context to populate the locale.
Step 3
I am stuck on how to get the request context to populate the locale in the SpEL extension as shown in step 2. This is what I try:
override fun getExtension(): Mono<out EvaluationContextExtension> {
return Mono.deferContextual { it.get<Mono<ServerWebExchange>>(ServerWebExchangeContextFilter.EXCHANGE_CONTEXT_ATTRIBUTE) }
.map { it.localeContext.locale?.language ?: "en" }
.map { LocaleAwareSpELExtension(it) }
}
but the exchange is not available in the context:
Context does not contain key: org.springframework.web.filter.reactive.ServerWebExchangeContextFilter.EXCHANGE_CONTEXT
From what I understand the context is not populated, because the Mono is not returned from the spring controller but it is used in the Query SpEL and I guess this is not the same context chain?
I think it must be possible, because it is similar to how the principal and security context would be made available, but I not fully understand how ReactiveSecurityContextHolder works - especially not how exactly it is populated for the SpEL context. In fact, in my default configuration it is not even populated - again I think because it is not the same context chain. Another difference here is that spring explicitly sets the security context to the reactive context in the ReactorContextWebFilter.
I think I now could create my own reactive request filter to populate the locale context, but I don't know how to make the reactive context available to the SpEL context.
Turns out adding the filter works (s. below). I still don't fully understand how the context population of SpEL VS reactive context works in this scenario, but it works..
#Component
class ReactorContextLocaleWebFilter : WebFilter {
companion object {
val KEY = ReactorContextLocaleWebFilter::class.java
}
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return chain.filter(exchange).contextWrite { context: Context ->
if (context.hasKey(KEY))
context
else
withLocaleContext(context, exchange)
}
}
private fun withLocaleContext(mainContext: Context, exchange: ServerWebExchange): Context {
return mainContext
.putAll(Mono.just(exchange.localeContext.locale?.language ?: "en").`as` { lang: Mono<String> ->
Context.of(KEY, lang).readOnly()
})
}
}
and then adjusting the extension creation to use the web filter:
override fun getExtension(): Mono<out EvaluationContextExtension> {
return Mono.deferContextual { it.get<Mono<String>>(ReactorContextLocaleWebFilter.KEY) }
.map { LocaleAwareSpELExtension(it) }
}

Is it possible to use Spring HATEOAS WebFluxLinkBuilders with Kotlin Coroutines?

I am trying to translate the following reactive code into kotlin coroutines:
#GetMapping
fun getAllTodosMono(): Mono<CollectionModel<TodoItem>> =
repository
.findAll()
.collectList()
.flatMap { mkSelfLinkMono(it) }
private fun mkSelfLinkMono(list: List<TodoItem>): Mono<CollectionModel<TodoItem>> {
val method = methodOn(Controller::class.java).getAllTodosMono()
val selfLink = linkTo(method).withSelfRel().toMono()
return selfLink.map { CollectionModel.of(list, it) }
}
Coroutine Version:
#GetMapping
suspend fun getAllTodosCoroutine(): CollectionModel<TodoItem> =
repository
.findAll()
.collectList()
.awaitSingle()
.let { mkSelfLinkCoroutine(it) }
private suspend fun mkSelfLinkCoroutine(list: List<TodoItem>): CollectionModel<TodoItem> {
val method = methodOn(Controller::class.java).getAllTodosCoroutine()
val selfLink = linkTo(method).withSelfRel().toMono().awaitSingle()
return CollectionModel.of(list, selfLink)
}
However, I get a runtime error when trying to run the code.
java.lang.ClassCastException: class org.springframework.hateoas.server.core.LastInvocationAware$$EnhancerBySpringCGLIB$$d8fd0e7e cannot be cast to class org.springframework.hateoas.CollectionModel (org.springframework.hateoas.server.core.LastInvocationAware$$EnhancerBySpringCGLIB$$d8fd0e7e is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader #62b177e9; org.springframework.hateoas.CollectionModel is in unnamed module of loader 'app')
I suspect methodOn(...) does not support suspend functions. The only solution that actually works is to build the link by hand instead of using the linkTo(...) function:
private fun mkSelfLink(list: List<TodoItem>): CollectionModel<TodoItem> {
return Link
.of("/api/v1/todos")
.withSelfRel()
.let { CollectionModel.of(list, it) }
}
However, I lose the ability to link to existing endpoints in my REST controller and also the host that is automagically added to the link uri.
Am I missing something?
EDIT: Here is the link to my github repo: https://github.com/enolive/kotlin-coroutines/tree/master/todos-coroutini
If you paste the following code sample into the TodoController replacing the original getTodo(...) method, you can see the failure I described above.
private suspend fun Todo.withSelfLinkByBuilder(): EntityModel<Todo> {
val method = methodOn(Controller::class.java).getTodo(id!!)
val selfLink = linkTo(method).withSelfRel().toMono().awaitSingle()
return EntityModel.of(this, selfLink)
}
#GetMapping("{id}")
suspend fun getTodo(#PathVariable id: ObjectId) =
repository.findById(id)?.withSelfLinkByBuilder()
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
Well, I found a solution, I don't know if is it a satisfactory one, but it works, none of the less.
By simple chaining the function calls together the runtime appears to work as intended:
private suspend fun mkSelfLinkCoroutine(list: List<TodoItem>): CollectionModel<TodoItem> {
val selfLink = linkTo(methodOn(Controller::class.java)
.getAllTodosCoroutine())
.withSelfRel()
.toMono()
.awaitSingle()
return CollectionModel.of(list, selfLink)
}
This is really strange, but it is what it is.
You probably forgot to add coroutines to your project. Add these dependencies to your gradle file:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")

Validating Spring Kafka payloads

I am trying to set up a service that has both a REST (POST) endpoint and a Kafka endpoint, both of which should take a JSON representation of the request object (let's call it Foo). I would want to make sure that the Foo object is valid (via JSR-303 or whatever). So Foo might look like:
public class Foo {
#Max(10)
private int bar;
// Getter and setter boilerplate
}
Setting up the REST endpoint is easy:
#PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> restEndpoint(#Valid #RequestBody Foo foo) {
// Do stuff here
}
and if I POST, { "bar": 9 } it processes the request, but if I post: { "bar": 99 } I get a BAD REQUEST. All good so far!
The Kafka endpoint is easy to create (along with adding a StringJsonMessageConverter() to my KafkaListenerContainerFactory so that I get JSON->Object conversion:
#KafkaListener(topics = "fooTopic")
public void kafkaEndpoint(#Valid #Payload Foo foo) {
// I shouldn't get here with an invalid object!!!
logger.debug("Successfully processed the object" + foo);
// But just to make sure, let's see if hand-validating it works
Validator validator = localValidatorFactoryBean.getValidator();
Set<ConstraintViolation<SlackMessage>> errors = validator.validate(foo);
if (errors.size() > 0) {
logger.debug("But there were validation errors!" + errors);
}
}
But no matter what I try, I can still pass invalid requests in and they process without error.
I've tried both #Valid and #Validated. I've tried adding a MethodValidationPostProcessor bean. I've tried adding a Validator to the KafkaListenerEndpointRegistrar (a la the EnableKafka javadoc):
#Configuration
public class MiscellaneousConfiguration implements KafkaListenerConfigurer {
private Logger logger = LoggerFactory.getLogger(this.getClass());
#Autowired
LocalValidatorFactoryBean validatorFactory;
#Override
public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) {
logger.debug("Configuring " + registrar);
registrar.setMessageHandlerMethodFactory(kafkaHandlerMethodFactory());
}
#Bean
public MessageHandlerMethodFactory kafkaHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setValidator(validatorFactory);
return factory;
}
}
I've now spent a few days on this, and I'm running out of other ideas. Is this even possible (without writing validation into every one of my kakfa endpoints)?
Sorry for the delay; we are at SpringOne Platform this week.
The infrastructure currently does not pass a Validator into the payload argument resolver. Please open an issue on GitHub.
Spring kafka listener by default do not scan for #Valid for non Rest controller classes. For more details please refer this answer
https://stackoverflow.com/a/71859991/13898185

Resources