Hi everyone, I’m facing a quite strange problem w...
# coroutines
a
Hi everyone, I’m facing a quite strange problem with spring WebClient + coroutines integration. I have the following method with webclient invocation:
Copy code
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:
Copy code
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:
Copy code
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
Copy code
public BusinessServiceException(String code, String title)
constructor is called, in onErrorMap() method. Second time, unexpectedly, the
Copy code
public BusinessServiceException(String message, Throwable cause)
constructor is called, somewhere in
Copy code
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
Copy code
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:
Copy code
/**
 * 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
Copy code
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
@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
@Evgeniy Zaharov yes, we are planning to do so
a
@Vsevolod Tolstopyatov [JB] what would you recommend as workaround for now? Disable stacktrace recovery?
v
@ARnikev yes. Another option is to make constructors
private
and replace it with factory function:
Copy code
class MyException private constructor(args)

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