Skip to content
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

suspend controller function returning Unit fails to serialize it #890

Closed
tmapes opened this issue Jul 15, 2024 · 5 comments
Closed

suspend controller function returning Unit fails to serialize it #890

tmapes opened this issue Jul 15, 2024 · 5 comments

Comments

@tmapes
Copy link

tmapes commented Jul 15, 2024

Expected Behavior

If an @Controller function is both suspend-able and returns Unit i would expect no body to be written to the caller.

Actual Behaviour

Default behavior seems to be to throw a serialization exception, however various @SerdeImport combinations appear to sometimes serialize Unit as an empty object ({}).

Attached repo is the former.

If a controller function returns Unit but isn't suspend-able it works without issue, no body is returned and no exception is thrown.

Stack Trace 18:21:31.401 [kotlinx.coroutines.DefaultExecutor] ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: Error encoding object [kotlin.Unit] to JSON: No serializable introspection present for type Unit. Consider adding Serdeable. Serializable annotate to type Unit. Alternatively if you are not in control of the project's source code, you can use @SerdeImport(Unit.class) to enable serialization of this type. io.micronaut.http.codec.CodecException: Error encoding object [kotlin.Unit] to JSON: No serializable introspection present for type Unit. Consider adding Serdeable. Serializable annotate to type Unit. Alternatively if you are not in control of the project's source code, you can use @SerdeImport(Unit.class) to enable serialization of this type. at io.micronaut.http.netty.body.NettyJsonHandler.writeTo(NettyJsonHandler.java:166) at io.micronaut.http.server.netty.RoutingInBoundHandler.writeNettyMessageBody(RoutingInBoundHandler.java:380) at io.micronaut.http.server.netty.RoutingInBoundHandler.encodeHttpResponse(RoutingInBoundHandler.java:358) at io.micronaut.http.server.netty.RoutingInBoundHandler.writeResponse(RoutingInBoundHandler.java:248) at io.micronaut.http.server.netty.NettyRequestLifecycle.lambda$handleNormal$0(NettyRequestLifecycle.java:101) at io.micronaut.http.reactive.execution.ReactorExecutionFlowImpl$1.onComplete(ReactorExecutionFlowImpl.java:121) at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onComplete(Operators.java:2231) at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:159) at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122) at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:74) at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:158) at reactor.core.publisher.MonoCompletionStage$MonoCompletionStageSubscription.apply(MonoCompletionStage.java:121) at reactor.core.publisher.MonoCompletionStage$MonoCompletionStageSubscription.apply(MonoCompletionStage.java:67) at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934) at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911) at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2179) at io.micronaut.http.bind.binders.CustomContinuation.resumeWith(ContinuationArgumentBinder.kt:120) at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:175) at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:164) at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:466) at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:500) at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:489) at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:587) at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:490) at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277) at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:105) at java.base/java.lang.Thread.run(Thread.java:1583) Caused by: io.micronaut.serde.exceptions.SerdeException: No serializable introspection present for type Unit. Consider adding Serdeable. Serializable annotate to type Unit. Alternatively if you are not in control of the project's source code, you can use @SerdeImport(Unit.class) to enable serialization of this type. at io.micronaut.serde.support.serializers.RuntimeTypeSerializer.lambda$getSerializer$0(RuntimeTypeSerializer.java:135) at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708) at io.micronaut.serde.support.serializers.RuntimeTypeSerializer.getSerializer(RuntimeTypeSerializer.java:131) at io.micronaut.serde.support.serializers.RuntimeTypeSerializer.serialize(RuntimeTypeSerializer.java:76) at io.micronaut.serde.jackson.JacksonJsonMapper.writeValue(JacksonJsonMapper.java:187) at io.micronaut.serde.jackson.JacksonJsonMapper.writeValue0(JacksonJsonMapper.java:178) at io.micronaut.serde.jackson.JacksonJsonMapper.writeValue0(JacksonJsonMapper.java:173) at io.micronaut.serde.jackson.JacksonJsonMapper.writeValue(JacksonJsonMapper.java:259) at io.micronaut.http.netty.body.NettyJsonHandler.writeTo(NettyJsonHandler.java:163) ... 27 common frames omitted Caused by: io.micronaut.core.beans.exceptions.IntrospectionException: No serializable introspection present for type Unit. Consider adding Serdeable. Serializable annotate to type Unit. Alternatively if you are not in control of the project's source code, you can use @SerdeImport(Unit.class) to enable serialization of this type. at io.micronaut.serde.support.DefaultSerdeIntrospections.getSerializableIntrospection(DefaultSerdeIntrospections.java:111) at io.micronaut.serde.support.serializers.SerBean.(SerBean.java:116) at io.micronaut.serde.support.serializers.ObjectSerializer.lambda$getSerializableBean$0(ObjectSerializer.java:159) at io.micronaut.core.util.SupplierUtil$2.get(SupplierUtil.java:79) at io.micronaut.serde.support.serializers.ObjectSerializer.getSerializableBean(ObjectSerializer.java:164) at io.micronaut.serde.support.serializers.ObjectSerializer.createSpecificInternal(ObjectSerializer.java:104) at io.micronaut.serde.support.serializers.ObjectSerializer.createSpecific(ObjectSerializer.java:96) at io.micronaut.serde.jackson.JacksonJsonMapper.writeValue(JacksonJsonMapper.java:185) ... 31 common frames omitted

Internal Server Error
io.micronaut.http.client.exceptions.HttpClientResponseException: Internal Server Error
at io.micronaut.http.client.netty.DefaultHttpClient$FullHttpResponseHandler.makeErrorFromRequestBody(DefaultHttpClient.java:2259)
at io.micronaut.http.client.netty.DefaultHttpClient$FullHttpResponseHandler.forwardResponseToPromise(DefaultHttpClient.java:2210)
at io.micronaut.http.client.netty.DefaultHttpClient$FullHttpResponseHandler.channelReadInstrumented(DefaultHttpClient.java:2179)
at io.micronaut.http.client.netty.DefaultHttpClient$FullHttpResponseHandler.channelReadInstrumented(DefaultHttpClient.java:2147)
at io.micronaut.http.client.netty.SimpleChannelInboundHandlerInstrumented.channelRead0(SimpleChannelInboundHandlerInstrumented.java:46)
at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:93)
at io.micronaut.http.client.netty.ResettableReadTimeoutHandler$NextInterceptor.channelRead(ResettableReadTimeoutHandler.java:92)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.handler.timeout.IdleStateHandler.channelRead(IdleStateHandler.java:289)
at io.micronaut.http.client.netty.ResettableReadTimeoutHandler.channelRead(ResettableReadTimeoutHandler.java:64)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1407)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:994)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:1583)
Suppressed: java.lang.Exception: #block terminated with an error
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:104)
at reactor.core.publisher.Flux.blockFirst(Flux.java:2766)
at io.micronaut.http.client.netty.DefaultHttpClient$1.exchange(DefaultHttpClient.java:571)
at io.micronaut.http.client.BlockingHttpClient.exchange(BlockingHttpClient.java:77)
at io.micronaut.http.client.BlockingHttpClient.exchange(BlockingHttpClient.java:106)
at com.example.UnitDemoTest.test_deleteAndReturnUnitdeleteAndReturnUnitSuspend(UnitDemoTest.kt:45)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at io.micronaut.test.extensions.junit5.MicronautJunit5Extension$2.proceed(MicronautJunit5Extension.java:142)
at io.micronaut.test.extensions.AbstractMicronautExtension.interceptEach(AbstractMicronautExtension.java:162)
at io.micronaut.test.extensions.AbstractMicronautExtension.interceptTest(AbstractMicronautExtension.java:119)
at io.micronaut.test.extensions.junit5.MicronautJunit5Extension.interceptTestMethod(MicronautJunit5Extension.java:129)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

Steps To Reproduce

  • Clone project
  • Set JDK/JRE to 21
  • Run included tests
./gradlew test

Environment Information

  • macOS (arm)
  • Java 21 & 17

Example Application

https://github.com/tmapes/mn-serde-unit

Version

4.5.0

@yawkat
Copy link
Member

yawkat commented Jul 17, 2024

Can you try with 4.5.1? There was a related fix recently, not sure if it helps micronaut-projects/micronaut-core#10953

@tmapes
Copy link
Author

tmapes commented Jul 18, 2024

@yawkat After some trial and error I was able to bump my local versions to the snapshots that include that PR but it didn't fix it.

After walking the call chain the RouteExecutor seems to think the return type despite explicitly being Unit in the controller, the isKotlinFunctionReturnTypeUnit value is being set to false.

Seems to come from this instanceof check, it's returning false.

image

@tmapes
Copy link
Author

tmapes commented Jul 18, 2024

It's then causing the check here to become truthy, thus causing the body to be set to Unit

image

@graemerocher
Copy link
Contributor

seems a core bug

@tmapes
Copy link
Author

tmapes commented Jul 18, 2024

Noted, I'll move to core.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants