Marty Pitt opened SPR-15083 and commented
When a Rest controller's @RequestBody
params can't be deserialized because of incorrect JSON, the error is lost through the reactive chain.
Specifically, when Jackson throws an exception during deserialization, the exception is lost.
In my specific example, this was a Kotlin class, and I didn't pass a value for a required param.
However, I suspect the same issue would occur with a malformed date, data type mismatch, etc.
A meaningful exception is thrown from Jackson (I see it caught in Jackson2JsonDecoder, line 121:
catch (IOException ex) {
return Flux.error(new CodecException("Error while reading the data", ex));
}
However, when this makes it's way back up to FluxMapFuseable, and into the Reactive land, this somehow gets lost, and the below exception is what we end up with.
It'd be great to see the original exception from Jackson be made available somehow, and - ideally - this result in a 4xx error.
Stack trace below:
java.lang.IllegalStateException: Failed to invoke controller with resolved arguments: [0][type=reactor.core.publisher.FluxOnAssembly][value={ operator : "OnAssembly" }] on method [public reactor.core.publisher.Mono<io.fullyloaded.conductor.plans.Plan> io.fullyloaded.conductor.plans.PlanController.addThing(io.fullyloaded.conductor.plans.Thing2)]
at org.springframework.web.reactive.result.method.InvocableHandlerMethod.lambda$invoke$0(InvocableHandlerMethod.java:112) ~[spring-web-reactive-5.0.0.BUILD-20170101.132107-475.jar:5.0.0.BUILD-SNAPSHOT]
at reactor.core.publisher.MonoThenMap$MonoThenApplyMain.onNext(MonoThenMap.java:98) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:316) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:990) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.MonoWhen$MonoWhenCoordinator.signal(MonoWhen.java:231) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.MonoWhen$MonoWhenSubscriber.onNext(MonoWhen.java:275) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:316) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:173) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:316) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:86) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:316) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:68) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:316) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxResume$ResumeSubscriber.onNext(FluxResume.java:71) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:316) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:990) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.MonoSingle$SingleSubscriber.onComplete(MonoSingle.java:161) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onComplete(FluxOnAssembly.java:327) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:150) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onComplete(FluxOnAssembly.java:327) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxFlatMap$FlatMapMain.checkTerminated(FluxFlatMap.java:798) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxFlatMap$FlatMapMain.drainLoop(FluxFlatMap.java:558) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxFlatMap$FlatMapMain.drain(FluxFlatMap.java:538) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxFlatMap$FlatMapMain.onComplete(FluxFlatMap.java:531) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onComplete(FluxOnAssembly.java:327) ~[reactor-core-3.0.4.RELEASE.jar:3.0.4.RELEASE]
at org.springframework.http.server.reactive.AbstractListenerReadPublisher$State.onAllDataRead(AbstractListenerReadPublisher.java:331) ~[spring-web-5.0.0.BUILD-20170101.132107-532.jar:5.0.0.BUILD-SNAPSHOT]
at org.springframework.http.server.reactive.AbstractListenerReadPublisher.onAllDataRead(AbstractListenerReadPublisher.java:86) ~[spring-web-5.0.0.BUILD-20170101.132107-532.jar:5.0.0.BUILD-SNAPSHOT]
at org.springframework.http.server.reactive.ServletServerHttpRequest$RequestBodyPublisher$RequestBodyPublisherReadListener.onAllDataRead(ServletServerHttpRequest.java:250) ~[spring-web-5.0.0.BUILD-20170101.132107-532.jar:5.0.0.BUILD-SNAPSHOT]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:361) ~[tomcat-embed-core-8.5.6.jar:8.5.6]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:784) ~[tomcat-embed-core-8.5.6.jar:8.5.6]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) ~[tomcat-embed-core-8.5.6.jar:8.5.6]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:802) ~[tomcat-embed-core-8.5.6.jar:8.5.6]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1410) ~[tomcat-embed-core-8.5.6.jar:8.5.6]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-8.5.6.jar:8.5.6]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) ~[na:1.8.0_65]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) ~[na:1.8.0_65]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-8.5.6.jar:8.5.6]
at java.lang.Thread.run(Thread.java:745) ~[na:1.8.0_65]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Assembly trace from producer [reactor.core.publisher.MonoError] :
reactor.core.publisher.Mono.error(Mono.java:274)
org.springframework.web.reactive.result.method.InvocableHandlerMethod.lambda$invoke$0(InvocableHandlerMethod.java:112)
reactor.core.publisher.MonoThenMap$MonoThenApplyMain.onNext(MonoThenMap.java:98)
reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:990)
reactor.core.publisher.MonoWhen$MonoWhenCoordinator.signal(MonoWhen.java:231)
reactor.core.publisher.MonoWhen$MonoWhenSubscriber.onNext(MonoWhen.java:275)
reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:173)
reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:86)
reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:68)
reactor.core.publisher.FluxResume$ResumeSubscriber.onNext(FluxResume.java:71)
reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:990)
reactor.core.publisher.MonoSingle$SingleSubscriber.onComplete(MonoSingle.java:161)
reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:150)
reactor.core.publisher.FluxFlatMap$FlatMapMain.checkTerminated(FluxFlatMap.java:798)
reactor.core.publisher.FluxFlatMap$FlatMapMain.drainLoop(FluxFlatMap.java:558)
reactor.core.publisher.FluxFlatMap$FlatMapMain.drain(FluxFlatMap.java:538)
reactor.core.publisher.FluxFlatMap$FlatMapMain.onComplete(FluxFlatMap.java:531)
org.springframework.http.server.reactive.AbstractListenerReadPublisher$State.onAllDataRead(AbstractListenerReadPublisher.java:331)
org.springframework.http.server.reactive.AbstractListenerReadPublisher.onAllDataRead(AbstractListenerReadPublisher.java:86)
org.springframework.http.server.reactive.ServletServerHttpRequest$RequestBodyPublisher$RequestBodyPublisherReadListener.onAllDataRead(ServletServerHttpRequest.java:250)
org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:784)
org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:802)
Composition chain until failing Operator :
|_ Mono.error(InvocableHandlerMethod.java:112)
Affects: 5.0 M4
Referenced from: commits https://github.com/spring-projects/spring-boot/commit/e399cf691888f311bbe886ff61703ca54004e0ca
Comment From: spring-projects-issues
Brian Clozel commented
How are you running your application?
If you're using plain Spring Framework, did you activate the "stacktrace mode" option with the following:
Hooks.onOperator(h -> h.operatorStacktrace());
If you're using the experimental boot starter, adding the Spring Boot Devtools dependency should enable that option.
Comment From: spring-projects-issues
Marty Pitt commented
It's a Spring Boot application, and I created it via start.spring.io with the experimental reactive-web.
A bunch of stuff that may help -- let me know if there's any other information I can provide:
- The parent is spring-boot-starter-parent 2.0.0.BUILD-SNAPSHOT
- I have devtools present in the pom
- Adding the Hooks.onOperator() call generated a runtime exception indicating this was already present
- I have a dependency on spring-boot-starter-web-reactive
- I'm using the annotation driver (
@RestController
) style of controllers, declaring return types as Mono\, rather than the functional style
Comment From: spring-projects-issues
Sébastien Deleuze commented
Indeed Spring MVC and Spring WebFlux behavior seems to be different here, and that makes debugging painful.
In addition to the lack of stacktrace, the JSON error output lack of useful information:
Spring MVC
{
"error": "Bad Request",
"message": "JSON parse error: Instantiation of [simple type, class com.example.demojpakotlin.Foo] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.example.demojpakotlin.Foo] value failed for JSON property name due to missing (therefore NULL) value for creator parameter name which is a non-nullable type\n at [Source: (PushbackInputStream); line: 1, column: 19] (through reference chain: com.example.demojpakotlin.Foo[\"name\"])",
"path": "/",
"status": 400,
"timestamp": "2018-01-26T15:43:54.885+0000"
}
Spring WebFlux
{
"error": "Bad Request",
"message": "Failed to read HTTP message",
"path": "/",
"status": 400,
"timestamp": "2018-01-26T15:44:38.645+0000"
}
Comment From: spring-projects-issues
Sébastien Deleuze commented
With this commit (not merged yet) WebFlux provides the detailed message of the original exception like MVC does in the HTTP response body.
Printing the exception in the logs seems to be more on Spring Boot side via DefaultErrorWebExceptionHandler#logError
which currently prints only 5xx
error with an error
level. I am wondering if it should also print 4xx
error with an warn
level in order to be consistent with what happens on MVC side and make it easier to find the root cause of such application error which is currently pretty hard.
Any thoughts Rossen Stoyanchev Brian Clozel?
Comment From: spring-projects-issues
Sébastien Deleuze commented
Additional info : with Spring MVC, logging is performed by DefaultHandlerExceptionResolver#handleHttpMessageNotReadable
and exception type is HttpMessageNotReadableException
.
With WebFlux, the exception is a ServerWebInputException
but is currently not logged because status code is 4xx not 5xx.
Comment From: spring-projects-issues
Sébastien Deleuze commented
I have draft WebFlux and Boot commits in order to discuss on a more concrete proposal.
Comment From: spring-projects-issues
Sébastien Deleuze commented
Pull request submitted for review by Rossen Stoyanchev and Brian Clozel.
Comment From: spring-projects-issues
Sébastien Deleuze commented
Merged with this commit (wrong issue mentioned in the commit log :-(, sorry for that) as a general improvement of WebFlux exception logging : 400 errors are now logged, no stack trace only the message for ResponseStatusException
and the request method and uri are now logged for more contextual error messages.