micronaut-projects / micronaut-serialization

Build Time Serialization APIs for Micronaut
Apache License 2.0
26 stars 19 forks source link

`suspend` controller function returning `Unit` fails to serialize it #890

Closed tmapes closed 2 months ago

tmapes commented 2 months ago

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

Environment Information

Example Application

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

Version

4.5.0

yawkat commented 2 months ago

Can you try with 4.5.1? There was a related fix recently, not sure if it helps https://github.com/micronaut-projects/micronaut-core/pull/10953

tmapes commented 2 months ago

@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 commented 2 months ago

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

image

graemerocher commented 2 months ago

seems a core bug

tmapes commented 2 months ago

Noted, I'll move to core.