https://kotlinlang.org logo
Title
a

ARnikev

01/29/2019, 10:17 AM
Hi everyone, I’m facing a quite strange problem with spring WebClient + coroutines integration. I have the following method with webclient invocation:
fun doSomething(someBody: Body): Mono<Void> {
  return client
			.patch()
			.uri("/someuri")
			.accept(APPLICATION_JSON_UTF8)
			.contentType(APPLICATION_JSON_UTF8)
			.body(BodyInserters.fromObject(someBody))
			.retrieve()
			.bodyToMono(Void::class.java)
			.onErrorMap {
				BusinessServiceException("CODE", "TITLE").apply {
					httpStatus = HttpStatus.FORBIDDEN
					detail = "some detail"
				}
			}
}
To invoke this method in non-blocking way i do following thing:
try {
  clientBean.doSomething(someBody).awaitFirst()
} catch (e: Exception) {
  throw e
}
What i expect is that in case of exception in webclient call there has to be BusinessServiceException caught in catch block above. And BusinessServiceException fields (code, title, httpStatus, detail) have to be populated with values that i’ve set in onErrorMap method on webclient mono response. What i really have is BusinessServiceException with null fields (code, title, httpStatus, detail) and cause field populated with expected by me BusinessServiceException (with all fields filled the same way as i’ve set them in onErrorMap method). BusinessServiceException class looks like this:
public class BusinessServiceException extends ServiceException {
    public BusinessServiceException(String code, String title) {
        super(code, title);
    }
    public BusinessServiceException(String message, Throwable cause) {
        super(message, cause);
    }
}
I’ve tried to debug all this stuff and it seems like that BusinessServiceException’s constructors called 2 times. First time, as expected, the
public BusinessServiceException(String code, String title)
constructor is called, in onErrorMap() method. Second time, unexpectedly, the
public BusinessServiceException(String message, Throwable cause)
constructor is called, somewhere in
private suspend fun <T> Publisher<T>.awaitOne -> override fun onError(e: Throwable) { cont.resumeWithException(e)}
method, and that call wraps original exception in exception of the same type but with null fields (because of wrong constructor call). I couldn’t find the exact place where this constructor could be called. Interesting thing is if i delete this second constructor
public BusinessServiceException(String message, Throwable cause)
from the BusinessServiceException.class - everything works as expected. I would really appreciate any thoughts on this.
Okay, it seems like i’ve figured it out. It has to do with stacktrace recovery mechanism:
/**
 * Tries to recover stacktrace for given [exception] and [continuation].
 * Stacktrace recovery tries to restore [continuation] stack frames using its debug metadata with [CoroutineStackFrame] API
 * and then reflectively instantiate exception of given type with original exception as a cause and
 * sets new stacktrace for wrapping exception.
 * Some frames may be missing due to tail-call elimination.
 *
 * Works only on JVM with enabled debug-mode.
 */
internal expect fun <E: Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E
Exception is instantiated using reflection by using no-arg, cause or cause and message constructor.
It uses different constructor for exception recovery and therefore loses original exception’s fields data.
e

Evgeniy Zaharov

01/29/2019, 1:24 PM
@Vsevolod Tolstopyatov [JB] mb be better to add special interface with method for exception cloning, and if exception implements this interface then this method will be used for exception cloning?
v

Vsevolod Tolstopyatov [JB]

01/29/2019, 2:14 PM
@Evgeniy Zaharov yes, we are planning to do so
a

ARnikev

01/29/2019, 5:14 PM
@Vsevolod Tolstopyatov [JB] what would you recommend as workaround for now? Disable stacktrace recovery?
v

Vsevolod Tolstopyatov [JB]

01/30/2019, 12:00 PM
@ARnikev yes. Another option is to make constructors
private
and replace it with factory function:
class MyException private constructor(args)

// Factory that looks like constructor
fun MyException(args): MyException = ...
👌 1