Skip to content

Kotlin coroutines handler don't work with "org.springframework.boot:spring-boot-starter-validation" #344

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
nenros opened this issue Mar 29, 2022 · 20 comments
Labels
for: external-project Needs a change in external project

Comments

@nenros
Copy link

nenros commented Mar 29, 2022

Hi!
I have problems with Webflux + Kotlin + Coroutines. When I have something like that:

@Controller
class PersonController(val service: PersonService) {

    @MutationMapping( "createPerson")
    suspend fun createPerson(@Argument("input") personInput: PersonInput) : Person {
            return service.createPerson()
    }
}

and I try to make request

mutation {
    createPerson(input: {firstName: "Test", lastName: "Test"}) {
        id, firstName, lastName
    }
}

I get

{
  "errors": [
    {
      "message": "Index 1 out of bounds for length 1",
      "locations": [
        {
          "line": 33,
          "column": 5
        }
      ],
      "path": [
        "createPerson"
      ],
      "extensions": {
        "classification": "INTERNAL_ERROR"
      }
    }
  ],
  "data": {
    "createPerson": null
  }
}

But when I change to response type mono

@Controller
class PersonController(val service: PersonService) {

    @MutationMapping( "createPerson")
    fun createPerson(@Argument("input") personInput: PersonInput) : Mono<Person> {
            return mono {service.createPerson()}
    }
}

request works


After some investigation I found that problem was in validation starter:

java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
	at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165) ~[na:na]
	at org.hibernate.validator.internal.properties.javabean.JavaBeanExecutable.getParameterName(JavaBeanExecutable.java:86) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ParameterMetaData$Builder.build(ParameterMetaData.java:165) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.findParameterMetaData(ExecutableMetaData.java:436) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.build(ExecutableMetaData.java:391) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataBuilder$BuilderDelegate.build(BeanMetaDataBuilder.java:260) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataBuilder.build(BeanMetaDataBuilder.java:133) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.createBeanMetaData(BeanMetaDataManagerImpl.java:206) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.getBeanMetaData(BeanMetaDataManagerImpl.java:165) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:267) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:235) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.springframework.graphql.data.method.annotation.support.HandlerMethodInputValidator.validate(HandlerMethodInputValidator.java:78) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.data.method.annotation.support.DataFetcherHandlerMethod.validateAndInvoke(DataFetcherHandlerMethod.java:188) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.data.method.annotation.support.DataFetcherHandlerMethod.invoke(DataFetcherHandlerMethod.java:121) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer$SchemaMappingDataFetcher.get(AnnotatedControllerConfigurer.java:449) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.execution.ContextDataFetcherDecorator.get(ContextDataFetcherDecorator.java:67) ~[spring-graphql-1.0.0-M6.jar:na]
	at graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation.lambda$instrumentDataFetcher$0(DataLoaderDispatcherInstrumentation.java:87) ~[graphql-java-17.3.jar:na]
	at graphql.execution.ExecutionStrategy.fetchField(ExecutionStrategy.java:279) ~[graphql-java-17.3.jar:na]
	at graphql.execution.ExecutionStrategy.resolveFieldWithInfo(ExecutionStrategy.java:210) ~[graphql-java-17.3.jar:na]
	at graphql.execution.ExecutionStrategy.resolveField(ExecutionStrategy.java:182) ~[graphql-java-17.3.jar:na]
	at graphql.execution.AsyncSerialExecutionStrategy.lambda$execute$1(AsyncSerialExecutionStrategy.java:43) ~[graphql-java-17.3.jar:na]
	at graphql.execution.Async.eachSequentiallyImpl(Async.java:80) ~[graphql-java-17.3.jar:na]
	at graphql.execution.Async.eachSequentially(Async.java:69) ~[graphql-java-17.3.jar:na]
	at graphql.execution.AsyncSerialExecutionStrategy.execute(AsyncSerialExecutionStrategy.java:38) ~[graphql-java-17.3.jar:na]
	at graphql.execution.Execution.executeOperation(Execution.java:159) ~[graphql-java-17.3.jar:na]
	at graphql.execution.Execution.execute(Execution.java:105) ~[graphql-java-17.3.jar:na]
	at graphql.GraphQL.execute(GraphQL.java:613) ~[graphql-java-17.3.jar:na]
	at graphql.GraphQL.parseValidateAndExecute(GraphQL.java:538) ~[graphql-java-17.3.jar:na]
	at graphql.GraphQL.executeAsync(GraphQL.java:502) ~[graphql-java-17.3.jar:na]
	at org.springframework.graphql.execution.DefaultExecutionGraphQlService.lambda$execute$2(DefaultExecutionGraphQlService.java:81) ~[spring-graphql-1.0.0-M6.jar:na]
	at reactor.core.publisher.MonoDeferContextual.subscribe(MonoDeferContextual.java:47) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:151) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:295) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:159) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:400) ~[reactor-netty-core-1.0.17.jar:1.0.17]
	at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:419) ~[reactor-netty-core-1.0.17.jar:1.0.17]
	at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:600) ~[reactor-netty-http-1.0.17.jar:1.0.17]
	at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93) ~[reactor-netty-core-1.0.17.jar:1.0.17]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:266) ~[reactor-netty-http-1.0.17.jar:1.0.17]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:327) ~[netty-codec-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:299) ~[netty-codec-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986) ~[netty-common-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.75.Final.jar:4.1.75.Final]
	at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

and this is wrong line

.validateParameters(handlerMethod.getBean(), handlerMethod.getMethod(), arguments, validationGroups);

question if it isn't related to https://hibernate.atlassian.net/browse/HV-1796

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Mar 29, 2022
@koenpunt
Copy link
Contributor

Are you using the latest version of the library? I remember having some issues with coroutines, but with the later versions this all works fine.

@nenros
Copy link
Author

nenros commented Mar 29, 2022

everything from milestone:

plugins {
    id("org.springframework.boot") version "2.7.0-M3"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.6.10"
    kotlin("plugin.spring") version "1.6.10"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenCentral()
    maven { url = uri("https://repo.spring.io/milestone") }
}

@koenpunt
Copy link
Contributor

koenpunt commented Mar 29, 2022

I think the issue lies somewhere else in your code; if I create a sample app using https://start.spring.io and create something similar as what you commented above everything is working fine.

data class PersonInput(
    val firstName: String,
    val lastName: String
)

@Controller
class PersonController(private val personService: PersonService) {
    @MutationMapping("createPerson")
    suspend fun createPerson(@Argument("input") personInput: PersonInput): Person {
        return personService.createPerson()
    }
}

// ...

@Service
class PersonService {
    fun createPerson(): Person {
        return Person(firstName = "Foo")
    }
}

// ...

data class Person(val firstName: String)
# src/main/resources/graphql/schema.graphqls

schema {
    query: Query
    mutation: Mutation
}

type Query {
    hello: String
}

input PersonInput {
    firstName: String
    lastName: String
}

type Person {
    id: ID
    firstName: String
    lastName: String
}

type Mutation {
    createPerson(input: PersonInput): Person
}

@nenros
Copy link
Author

nenros commented Mar 29, 2022

Ok I found problem. It is because of

.validateParameters(handlerMethod.getBean(), handlerMethod.getMethod(), arguments, validationGroups);
this method

and stack trace:

java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
	at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165) ~[na:na]
	at org.hibernate.validator.internal.properties.javabean.JavaBeanExecutable.getParameterName(JavaBeanExecutable.java:86) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ParameterMetaData$Builder.build(ParameterMetaData.java:165) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.findParameterMetaData(ExecutableMetaData.java:436) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.build(ExecutableMetaData.java:391) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataBuilder$BuilderDelegate.build(BeanMetaDataBuilder.java:260) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataBuilder.build(BeanMetaDataBuilder.java:133) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.createBeanMetaData(BeanMetaDataManagerImpl.java:206) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.getBeanMetaData(BeanMetaDataManagerImpl.java:165) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:267) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:235) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.springframework.graphql.data.method.annotation.support.HandlerMethodInputValidator.validate(HandlerMethodInputValidator.java:78) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.data.method.annotation.support.DataFetcherHandlerMethod.validateAndInvoke(DataFetcherHandlerMethod.java:188) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.data.method.annotation.support.DataFetcherHandlerMethod.invoke(DataFetcherHandlerMethod.java:121) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer$SchemaMappingDataFetcher.get(AnnotatedControllerConfigurer.java:449) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.execution.ContextDataFetcherDecorator.get(ContextDataFetcherDecorator.java:67) ~[spring-graphql-1.0.0-M6.jar:na]
	at graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation.lambda$instrumentDataFetcher$0(DataLoaderDispatcherInstrumentation.java:87) ~[graphql-java-17.3.jar:na]
	at graphql.execution.ExecutionStrategy.fetchField(ExecutionStrategy.java:279) ~[graphql-java-17.3.jar:na]
	at graphql.execution.ExecutionStrategy.resolveFieldWithInfo(ExecutionStrategy.java:210) ~[graphql-java-17.3.jar:na]
	at graphql.execution.ExecutionStrategy.resolveField(ExecutionStrategy.java:182) ~[graphql-java-17.3.jar:na]
	at graphql.execution.AsyncSerialExecutionStrategy.lambda$execute$1(AsyncSerialExecutionStrategy.java:43) ~[graphql-java-17.3.jar:na]
	at graphql.execution.Async.eachSequentiallyImpl(Async.java:80) ~[graphql-java-17.3.jar:na]
	at graphql.execution.Async.eachSequentially(Async.java:69) ~[graphql-java-17.3.jar:na]
	at graphql.execution.AsyncSerialExecutionStrategy.execute(AsyncSerialExecutionStrategy.java:38) ~[graphql-java-17.3.jar:na]
	at graphql.execution.Execution.executeOperation(Execution.java:159) ~[graphql-java-17.3.jar:na]
	at graphql.execution.Execution.execute(Execution.java:105) ~[graphql-java-17.3.jar:na]
	at graphql.GraphQL.execute(GraphQL.java:613) ~[graphql-java-17.3.jar:na]
	at graphql.GraphQL.parseValidateAndExecute(GraphQL.java:538) ~[graphql-java-17.3.jar:na]
	at graphql.GraphQL.executeAsync(GraphQL.java:502) ~[graphql-java-17.3.jar:na]
	at org.springframework.graphql.execution.DefaultExecutionGraphQlService.lambda$execute$2(DefaultExecutionGraphQlService.java:81) ~[spring-graphql-1.0.0-M6.jar:na]
	at reactor.core.publisher.MonoDeferContextual.subscribe(MonoDeferContextual.java:47) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:151) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:295) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:159) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:400) ~[reactor-netty-core-1.0.17.jar:1.0.17]
	at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:419) ~[reactor-netty-core-1.0.17.jar:1.0.17]
	at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:600) ~[reactor-netty-http-1.0.17.jar:1.0.17]
	at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93) ~[reactor-netty-core-1.0.17.jar:1.0.17]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:266) ~[reactor-netty-http-1.0.17.jar:1.0.17]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:327) ~[netty-codec-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:299) ~[netty-codec-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986) ~[netty-common-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.75.Final.jar:4.1.75.Final]
	at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

@nenros nenros changed the title Suspend function not found as handler. Kotlin coroutines handler don't work with "org.springframework.boot:spring-boot-starter-validation" Mar 29, 2022
@koenpunt
Copy link
Contributor

You're using @Valid annotations in your PersonInput class? Can you share all the relevant details of your application?

@nenros
Copy link
Author

nenros commented Mar 29, 2022

No, funniest fact is that I haven't added validation yet, only package.

I have similar problem with query mapping.

@Controller
class PersonController(val service: PersonService) {

    @QueryMapping
    suspend fun people(): List<Person> {
            return service.getAllPeople()
    }
    
    @MutationMapping( "createPerson")
    suspend fun createPerson(@Argument("input") personInput: PersonInput) : Person {
            return Person(firstName = personInput.firstName, lastName = personInput.lastName, id = null)
    }
}

and error:

2022-03-29 19:51:40.873 DEBUG 78428 --- [ctor-http-nio-2] s.g.e.ExceptionResolversExceptionHandler : Resolving exception

java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
	at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165) ~[na:na]
	at org.hibernate.validator.internal.properties.javabean.JavaBeanExecutable.getParameterName(JavaBeanExecutable.java:86) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ParameterMetaData$Builder.build(ParameterMetaData.java:165) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.findParameterMetaData(ExecutableMetaData.java:436) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.build(ExecutableMetaData.java:391) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataBuilder$BuilderDelegate.build(BeanMetaDataBuilder.java:260) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataBuilder.build(BeanMetaDataBuilder.java:133) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.createBeanMetaData(BeanMetaDataManagerImpl.java:206) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.getBeanMetaData(BeanMetaDataManagerImpl.java:165) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:267) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:235) ~[hibernate-validator-6.2.3.Final.jar:6.2.3.Final]
	at org.springframework.graphql.data.method.annotation.support.HandlerMethodInputValidator.validate(HandlerMethodInputValidator.java:78) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.data.method.annotation.support.DataFetcherHandlerMethod.validateAndInvoke(DataFetcherHandlerMethod.java:188) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.data.method.annotation.support.DataFetcherHandlerMethod.invoke(DataFetcherHandlerMethod.java:121) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer$SchemaMappingDataFetcher.get(AnnotatedControllerConfigurer.java:449) ~[spring-graphql-1.0.0-M6.jar:na]
	at org.springframework.graphql.execution.ContextDataFetcherDecorator.get(ContextDataFetcherDecorator.java:67) ~[spring-graphql-1.0.0-M6.jar:na]
	at graphql.execution.ExecutionStrategy.fetchField(ExecutionStrategy.java:279) ~[graphql-java-17.3.jar:na]
	at graphql.execution.ExecutionStrategy.resolveFieldWithInfo(ExecutionStrategy.java:210) ~[graphql-java-17.3.jar:na]
	at graphql.execution.AsyncExecutionStrategy.execute(AsyncExecutionStrategy.java:60) ~[graphql-java-17.3.jar:na]
	at graphql.execution.Execution.executeOperation(Execution.java:159) ~[graphql-java-17.3.jar:na]
	at graphql.execution.Execution.execute(Execution.java:105) ~[graphql-java-17.3.jar:na]
	at graphql.GraphQL.execute(GraphQL.java:613) ~[graphql-java-17.3.jar:na]
	at graphql.GraphQL.parseValidateAndExecute(GraphQL.java:538) ~[graphql-java-17.3.jar:na]
	at graphql.GraphQL.executeAsync(GraphQL.java:502) ~[graphql-java-17.3.jar:na]
	at org.springframework.graphql.execution.DefaultExecutionGraphQlService.lambda$execute$2(DefaultExecutionGraphQlService.java:81) ~[spring-graphql-1.0.0-M6.jar:na]
	at reactor.core.publisher.MonoDeferContextual.subscribe(MonoDeferContextual.java:47) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:151) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:295) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:159) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.16.jar:3.4.16]
	at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:400) ~[reactor-netty-core-1.0.17.jar:1.0.17]
	at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:419) ~[reactor-netty-core-1.0.17.jar:1.0.17]
	at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:600) ~[reactor-netty-http-1.0.17.jar:1.0.17]
	at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93) ~[reactor-netty-core-1.0.17.jar:1.0.17]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.75.Final.jar:4.1.75.Final]
	at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:266) ~[reactor-netty-http-1.0.17.jar:1.0.17

What you expect to share?

@hantsy
Copy link
Contributor

hantsy commented Mar 30, 2022

It is an old issues when using validation with Kotlin Coroutines. And there are some solutions in the original issues. For Kotlin Coroutines, when compiling, there is an additional Continuation is added in the method args.

There are several issues like this in Spring issue tracker, but they did think it is should be fixed by the upstream projects.

@nenros Check my example includes a solution from the issue comments, https://github.com/hantsy/spring-puzzles/tree/master/validation-ktco I think you can use the same method to resolve this issue.

@nenros
Copy link
Author

nenros commented Mar 30, 2022

@hantsy I think that problem is in spring-graphql as this

@RestController
@RequestMapping("/person")
class PersonRestController(val personService: PersonService) {
    
    @GetMapping
    suspend fun listPeople(): Flow<Person> {
        return personService.getAllPeople()
    }
    
    @PostMapping
    suspend fun createPerson(@Valid @RequestBody personInput: PersonInput) : Person {
        return personService.createPerson();
    }
}

works correctly

@koenpunt
Copy link
Contributor

@nenros since your stack trace shows org.hibernate.validator, it might be your entity class that's the problem here. Mind sharing that?

I'm using org.springframework.boot:spring-boot-starter-validation without any issues whatsoever.

@hantsy
Copy link
Contributor

hantsy commented Mar 30, 2022

@nenros Your example is validating the request body, if you validate the request param in the method, you will encounter this problem. spring-projects/spring-framework#23499 (comment)

It is a little wired, spring uses different mechanism to process the arg validation and request body validation.

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Mar 30, 2022

Indeed what @hantsy shared has links to issues that are referring to the same stacktrace. Originally reported in spring-projects/spring-framework#23499 and then corresponding issue in HV https://hibernate.atlassian.net/browse/HV-1638.

It is a lower level issue, we're simply invoking Hibernate Validator which fails while introspecting method parameters. There is one workaround posted in spring-projects/spring-framework#23499 (comment) customizing parameter discovery, but I have not tried or evaluated it.

No, funniest fact is that I haven't added validation yet, only package.

This is a good point @nenros as currently we are invoking HV for every method as long as bean validation is present but could consider doing so only for methods with @Validated on when any method parameter has @Valid.

@nenros
Copy link
Author

nenros commented Mar 30, 2022

adding magic configuration helped

@rstoyanchev
Copy link
Contributor

I've updated the documentation on this.

@rstoyanchev rstoyanchev added for: external-project Needs a change in external project and removed status: waiting-for-triage An issue we've not yet triaged labels Apr 19, 2022
@turboezh
Copy link
Contributor

Hi.
@rstoyanchev, I've found that any suspend function in controller, even private, not validated and not related to the query lead to this problem:

class Foo(
    val bar: Boolean = true
)

@Controller
class SuspendTestController {
    @QueryMapping
    fun foo(): Foo {
        return Foo()
    }

    private suspend fun theSuspend(): String {
        return "123"
    }
}

I think this is a very breaking behavior.

@rstoyanchev
Copy link
Contributor

@turboezh you must have javax.validation:validation-api on the classpath. Do you know what brings it in?

@turboezh
Copy link
Contributor

@rstoyanchev, yes, I need a validation so I have org.springframework.boot:spring-boot-starter-validation in my project.

I discovered that declaring such a bean magically fixes the problem:

    @Bean
    fun validator(): Validator {
        return Validation.buildDefaultValidatorFactory().validator
    }

I took it from org.springframework.validation.beanvalidation.MethodValidationInterceptor default constructor.

Eventually it leads to utilizing org.hibernate.validator.internal.engine.DefaultParameterNameProvider instead of org.springframework.core.DefaultParameterNameDiscoverer from LocalValidatorFactoryBean.

In fail case org.springframework.core.DefaultParameterNameDiscoverer delegates to org.springframework.core.KotlinReflectionParameterNameDiscoverer that returns parameter list without last continuation parameter. And it become inconsistent with org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData that counts parameters with last continuation parameter.
With HV DefaultParameterNameProvider it seems to work correctly.

So I see this error not in the Spring and not in HV. It is in communication between them.

In some other projects I use to validate GraphQL controllers with MethodValidationPostProcessor that creates MethodValidationInterceptor with default Validation.buildDefaultValidatorFactory() and I have no any issues with it so far.

Is there any benefits from "manually" invoking HV for every method (as you mentioned) instead of just using AOP MethodValidationPostProcessor with @Validated controllers?

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Jul 20, 2022

Thanks for the commenting. I can't really comment on your approach. It sounds like what you have works for you, but there be cases that don't if you exclude KotlinReflectionParameterNameDiscoverer. It might be a good idea to comment under spring-framework#23499 as we can't do much about DefaultParameterNameDiscoverer here. I also see that HV-1638 is not yet fixed.

In terms of not relying on MethodValidationPostProcessor, it's mainly about making validation is a built in feature that doesn't require AOP. There isn't anything else to it.

I wonder if we should simply suppress validation for coroutine methods while this remains an issue? We could further couple that we checks on startup to reject coroutine methods that method or parameter validation annotations.

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Jul 20, 2022

On further thought and some internal discussion, I've created #445, which should address the points you're raising about control and validation via AOP vs the built-in support. In effect, validation should become opt-in and will not impact methods that aren't expressing using any validation annotations.

@turboezh
Copy link
Contributor

I wonder if we should simply suppress validation for coroutine methods while this remains an issue? We could further couple that we checks on startup to reject coroutine methods that method or parameter validation annotations.

Sorry, I forgot to describe one more important detail that I mentioned in my first comment. HV ExecutableMetaData builds metadata of entire bean (@Controller in this case), and asks org.springframework.core.DefaultParameterNameDiscoverer about every method of the bean, even private ones. So even we would validate methods selectively, it's enough to have just one validation and one suspend method in a @Controller to got this problem. Its mutual exclusive things for now as I see.

@rstoyanchev
Copy link
Contributor

Thanks for the tip, please continue the discussion under #445.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
for: external-project Needs a change in external project
Projects
None yet
Development

No branches or pull requests

6 participants