question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Cannot catch Exception from Server Stub

See original GitHub issue

I am attempting to install a default Exception handler for our gRPC server stubs. I have tried using both a java gRPC interceptor and coroutine exception handlers, but nothing seems to work. The following code snippet below replicates the issue.

val handler = CoroutineExceptionHandler { _, ex ->
  println("this never happens!!")
}

internal class TestGreeterGrpcService : GreeterGrpcKt.GreeterCoroutineImplBase(handler) {
  override suspend fun sayHello(request: HelloRequest): HelloReply = throw RuntimeException("boom")
}

Using the debugger I traced the issue to here

https://github.com/grpc/grpc-kotlin/blob/master/stub/src/main/java/io/grpc/kotlin/ServerCalls.kt#L229

It seems like the CoroutineContext provided to the stub is not used in the Flow that dispatches requests.

I’m fairly new to coroutines so maybe I’m doing something wrong.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:8
  • Comments:6

github_iconTop GitHub Comments

37reactions
mfickettcommented, Nov 13, 2020

@jonpeterson Thanks for sharing that! I was trying to use onHalfClose etc and finding they did nothing.

In the implementation you shared, is the LoggingServerCallListener superfluous? It’s what you’d want in grpc-java, right, but exceptions never show up in it in grpc-kotlin? So I think this would suffice:

package com.yourproject

import io.grpc.ForwardingServerCall
import io.grpc.Metadata
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import io.grpc.Status
import mu.KotlinLogging
import javax.inject.Singleton

private val logger = KotlinLogging.logger {}

/**
 * Log all exceptions thrown from gRPC endpoints, and adjust Status for known exceptions.
 */
@Singleton
class ExceptionInterceptor : ServerInterceptor {

    /**
     * When closing a gRPC call, extract any error status information to top-level fields. Also
     * log the cause of errors.
     */
    private class ExceptionTranslatingServerCall<ReqT, RespT>(
        delegate: ServerCall<ReqT, RespT>
    ) : ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(delegate) {

        override fun close(status: Status, trailers: Metadata) {
            if (status.isOk) {
                return super.close(status, trailers)
            }
            val cause = status.cause
            var newStatus = status

            logger.error(cause) { "Error handling gRPC endpoint." }

            if (status.code == Status.Code.UNKNOWN) {
                val translatedStatus = when (cause) {
                    is IllegalArgumentException -> Status.INVALID_ARGUMENT
                    is IllegalStateException -> Status.FAILED_PRECONDITION
                    else -> Status.UNKNOWN
                }
                newStatus = translatedStatus.withDescription(cause?.message).withCause(cause)
            }

            super.close(newStatus, trailers)
        }
    }

    override fun <ReqT : Any, RespT : Any> interceptCall(
        call: ServerCall<ReqT, RespT>,
        headers: Metadata,
        next: ServerCallHandler<ReqT, RespT>
    ): ServerCall.Listener<ReqT> {
        return next.startCall(ExceptionTranslatingServerCall(call), headers)
    }
}
10reactions
jonpetersoncommented, Sep 22, 2020

I got really stuck on this as well and was about to switch to grpc-java, but then saw how TransmitStatusRuntimeExceptionInterceptor implemented a custom ServerCall. I’m going to post my solution here to help anyone who comes across this in the future. There are some custom exceptions and extension functions (ex. getLogPrefix for request and caller ID tracing) that I’m using in my project, but I’ll leave them in just to give an idea of what I was getting at.

private val logger = KotlinLogging.logger {}

class LoggingAndExceptionTranslationServerInterceptor : ServerInterceptor {

    private class ExceptionTranslatingServerCall<ReqT, RespT>(
        delegate: ServerCall<ReqT, RespT>
    ) : ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(delegate) {

        override fun close(status: Status, trailers: Metadata) {
            val newStatus = if (!status.isOk) {
                val cause = status.cause
                logger.error(logger.nullIfNotDebug(cause)) { "${getLogPrefix()}closing due to error" }

                if (status.code == Status.Code.UNKNOWN) {
                    val newStatus = when (cause) {
                        is IllegalArgumentException -> Status.INVALID_ARGUMENT
                        is IllegalStateException -> Status.FAILED_PRECONDITION
                        is NotFoundException -> Status.NOT_FOUND
                        is ConflictException -> Status.ALREADY_EXISTS
                        is UnauthenticationException -> Status.UNAUTHENTICATED
                        is UnauthorizationException -> Status.PERMISSION_DENIED
                        else -> Status.UNKNOWN
                    }
                    newStatus.withDescription(cause?.message).withCause(cause)
                } else
                    status
            } else {
                logger.trace { "${getLogPrefix()}closing" }
                status
            }

            super.close(newStatus, trailers)
        }
    }

    private class LoggingServerCallListener<ReqT>(
        delegate: ServerCall.Listener<ReqT>
    ) : ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(delegate) {

        override fun onMessage(message: ReqT) {
            logger.trace { "${getLogPrefix()}message: $message" }
            try {
                super.onMessage(message)
            } catch (t: Throwable) {
                if (logger.isDebugEnabled)
                    logger.debug(t) { "${getLogPrefix()}error on message: $message" }
                else
                    logger.error { "${getLogPrefix()}error handling message" }
                throw t
            }
        }

        override fun onHalfClose() {
            logger.trace { "${getLogPrefix()}half-close" }
            try {
                super.onHalfClose()
            } catch (t: Throwable) {
                logger.error(logger.nullIfNotDebug(t)) { "${getLogPrefix()}error handling half-close" }
                throw t
            }
        }

        override fun onCancel() {
            logger.trace { "${getLogPrefix()}cancel" }
            try {
                super.onCancel()
            } catch (t: Throwable) {
                logger.error(logger.nullIfNotDebug(t)) { "${getLogPrefix()}error handling cancel" }
                throw t
            }
        }

        override fun onComplete() {
            logger.trace { "${getLogPrefix()}complete" }
            try {
                super.onComplete()
            } catch (t: Throwable) {
                logger.error(logger.nullIfNotDebug(t)) { "${getLogPrefix()}error handling complete" }
                throw t
            }
        }

        override fun onReady() {
            logger.trace { "${getLogPrefix()}ready" }
            try {
                super.onReady()
            } catch (t: Throwable) {
                logger.error(logger.nullIfNotDebug(t)) { "${getLogPrefix()}error handling ready" }
                throw t
            }
        }
    }

    override fun <ReqT : Any, RespT : Any> interceptCall(
        call: ServerCall<ReqT, RespT>,
        metadata: Metadata,
        next: ServerCallHandler<ReqT, RespT>
    ): ServerCall.Listener<ReqT> =
        LoggingServerCallListener(next.startCall(ExceptionTranslatingServerCall(call), metadata))
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

java - I can't throw NoRouteToHostException even with catch ...
You are not throwing any exceptions. You are catching all of them, then printing the stack and returning false.
Read more >
gRPC Error Handling | Vinsguru
Learn gRPC Error Handling with Java. Different options to respond with client when some error occurred using Status, metadata & oneof.
Read more >
StubNotFoundException - server program unable to load stub ...
I want to use dynamic code loading. Therefore, I've placed the stub class file in a direcotry accessible by the web server.
Read more >
Error Handling in gRPC - Baeldung
All client or server gRPC libraries support the official gRPC error model. Java encapsulates this error model with the class io.grpc.Status.
Read more >
Getting Error Handling right in gRPC - Techdozo
The responsibility of the client application (Product Gateway Service) is to call the server application and convert the received response to ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found