Last active
June 29, 2022 00:21
-
-
Save rponte/949d947ac3c38aa7181929c41ee56c05 to your computer and use it in GitHub Desktop.
Micronaut: Implementing a gRPC Server Interceptor for exception handling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.zup.edu.shared.grpc | |
import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener | |
import io.grpc.Metadata | |
import io.grpc.ServerCall | |
import io.grpc.ServerCallHandler | |
import io.grpc.ServerInterceptor | |
import org.slf4j.LoggerFactory | |
import javax.inject.Inject | |
import javax.inject.Singleton | |
/** | |
* TIP1: This interceptor has been tested with gRPC-Java, maybe it doesn't work with gRPC-Kotlin | |
* TIP2: I'm not sure if this interceptor works well with all kind of gRPC-flows, like client and/or server streaming | |
* TIP3: I think that implementing this interceptor via AOP would be better because we don't have to worry about the gRPC life-cycle | |
*/ | |
@Singleton | |
class ExceptionHandlerGrpcServerInterceptor(@Inject val resolver: ExceptionHandlerResolver) : ServerInterceptor { | |
private val logger = LoggerFactory.getLogger(ExceptionHandlerGrpcServerInterceptor::class.java) | |
override fun <ReqT : Any, RespT : Any> interceptCall( | |
call: ServerCall<ReqT, RespT>, | |
headers: Metadata, | |
next: ServerCallHandler<ReqT, RespT>, | |
): ServerCall.Listener<ReqT> { | |
fun handleException(call: ServerCall<ReqT, RespT>, e: Exception) { | |
logger.error("Handling exception $e while processing the call: ${call.methodDescriptor.fullMethodName}") | |
val handler = resolver.resolve(e) | |
val translatedStatus = handler.handle(e) | |
call.close(translatedStatus.status, translatedStatus.metadata) | |
} | |
val listener: ServerCall.Listener<ReqT> = try { | |
next.startCall(call, headers) | |
} catch (ex: Exception) { | |
handleException(call, ex) | |
throw ex | |
} | |
return object : SimpleForwardingServerCallListener<ReqT>(listener) { | |
// No point in overriding onCancel and onComplete; it's already too late | |
override fun onHalfClose() { | |
try { | |
super.onHalfClose() | |
} catch (ex: Exception) { | |
handleException(call, ex) | |
throw ex | |
} | |
} | |
override fun onReady() { | |
try { | |
super.onReady() | |
} catch (ex: Exception) { | |
handleException(call, ex) | |
throw ex | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.zup.edu.shared.grpc | |
import br.com.zup.edu.shared.grpc.handlers.DefaultExceptionHandler | |
import javax.inject.Inject | |
import javax.inject.Singleton | |
@Singleton | |
class ExceptionHandlerResolver( | |
@Inject private val handlers: List<ExceptionHandler<Exception>>, | |
) { | |
private var defaultHandler: ExceptionHandler<Exception> = DefaultExceptionHandler() | |
/** | |
* We can replace the default exception handler through this constructor | |
* https://docs.micronaut.io/latest/guide/index.html#replaces | |
*/ | |
constructor(handlers: List<ExceptionHandler<Exception>>, defaultHandler: ExceptionHandler<Exception>) : this(handlers) { | |
this.defaultHandler = defaultHandler | |
} | |
fun resolve(e: Exception): ExceptionHandler<Exception> { | |
val foundHandlers = handlers.filter { h -> h.supports(e) } | |
if (foundHandlers.size > 1) | |
throw IllegalStateException("Too many handlers supporting the same exception '${e.javaClass.name}': $foundHandlers") | |
return foundHandlers.firstOrNull() ?: defaultHandler | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.zup.edu.shared.grpc | |
import io.grpc.Metadata | |
import io.grpc.Status | |
import io.grpc.StatusRuntimeException | |
import io.grpc.protobuf.StatusProto | |
interface ExceptionHandler<E : Exception> { | |
/** | |
* Handles exception and maps it to StatusWithDetails | |
*/ | |
fun handle(e: E): StatusWithDetails | |
/** | |
* Verifies whether this instance can handle the specified exception or not | |
*/ | |
fun supports(e: Exception): Boolean | |
/** | |
* Simple wrapper for Status and Metadata (trailers) | |
*/ | |
data class StatusWithDetails(val status: Status, val metadata: Metadata = Metadata()) { | |
constructor(se: StatusRuntimeException): this(se.status, se.trailers ?: Metadata()) | |
constructor(sp: com.google.rpc.Status): this(StatusProto.toStatusRuntimeException(sp)) | |
fun asRuntimeException(): StatusRuntimeException { | |
return status.asRuntimeException(metadata) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.zup.edu.shared.grpc.handlers | |
import br.com.zup.edu.shared.grpc.ExceptionHandler | |
import br.com.zup.edu.shared.grpc.ExceptionHandler.StatusWithDetails | |
import io.grpc.Status | |
/** | |
* By design, this class must NOT be managed by Micronaut | |
*/ | |
class DefaultExceptionHandler : ExceptionHandler<Exception> { | |
override fun handle(e: Exception): StatusWithDetails { | |
val status = when (e) { | |
is IllegalArgumentException -> Status.INVALID_ARGUMENT.withDescription(e.message) | |
is IllegalStateException -> Status.FAILED_PRECONDITION.withDescription(e.message) | |
else -> Status.UNKNOWN | |
} | |
return StatusWithDetails(status.withCause(e)) | |
} | |
override fun supports(e: Exception): Boolean { | |
return true | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.zup.edu.shared.grpc.handlers | |
import br.com.zup.edu.shared.grpc.ExceptionHandler | |
import br.com.zup.edu.shared.grpc.ExceptionHandler.StatusWithDetails | |
import com.google.protobuf.Any | |
import com.google.rpc.BadRequest | |
import com.google.rpc.Code | |
import javax.inject.Singleton | |
import javax.validation.ConstraintViolationException | |
/** | |
* Handles the Bean Validation errors adding theirs violations into request trailers (metadata) | |
*/ | |
@Singleton | |
class ConstraintViolationExceptionHandler : ExceptionHandler<ConstraintViolationException> { | |
override fun handle(e: ConstraintViolationException): StatusWithDetails { | |
val details = BadRequest.newBuilder() | |
.addAllFieldViolations(e.constraintViolations.map { | |
BadRequest.FieldViolation.newBuilder() | |
.setField(it.propertyPath.last().name ?: "?? key ??") // still thinking how to solve this case | |
.setDescription(it.message) | |
.build() | |
}) | |
.build() | |
val statusProto = com.google.rpc.Status.newBuilder() | |
.setCode(Code.INVALID_ARGUMENT_VALUE) | |
.setMessage("Request with invalid data") | |
.addDetails(Any.pack(details)) | |
.build() | |
return StatusWithDetails(statusProto) | |
} | |
override fun supports(e: Exception): Boolean { | |
return e is ConstraintViolationException | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.zup.edu.shared.grpc.handlers | |
import br.com.zup.edu.shared.grpc.ExceptionHandler | |
import br.com.zup.edu.shared.grpc.ExceptionHandler.StatusWithDetails | |
import io.grpc.Status | |
import io.micronaut.context.MessageSource | |
import io.micronaut.context.MessageSource.MessageContext | |
import org.hibernate.exception.ConstraintViolationException | |
import javax.inject.Inject | |
import javax.inject.Singleton | |
/** | |
* The idea of this handler is to deal with database constraints errors, like unique or FK constraints for example | |
*/ | |
@Singleton | |
class DataIntegrityExceptionHandler(@Inject var messageSource: MessageSource) : ExceptionHandler<ConstraintViolationException> { | |
override fun handle(e: ConstraintViolationException): StatusWithDetails { | |
val constraintName = e.constraintName | |
if (constraintName.isNullOrBlank()) { | |
return internalServerError(e) | |
} | |
val message = messageSource.getMessage("data.integrity.error.$constraintName", MessageContext.DEFAULT) | |
return message | |
.map { alreadyExistsError(it, e) } // TODO: dealing with many types of constraint errors | |
.orElse(internalServerError(e)) | |
} | |
override fun supports(e: Exception): Boolean { | |
return e is ConstraintViolationException | |
} | |
private fun alreadyExistsError(message: String?, e: ConstraintViolationException) = | |
StatusWithDetails(Status.ALREADY_EXISTS | |
.withDescription(message) | |
.withCause(e)) | |
private fun internalServerError(e: ConstraintViolationException) = | |
StatusWithDetails(Status.INTERNAL | |
.withDescription("Unexpected internal server error") | |
.withCause(e)) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.zup.edu.shared.grpc.handlers | |
import br.com.zup.edu.pix.ChavePixNaoEncontradaException | |
import br.com.zup.edu.shared.grpc.ExceptionHandler | |
import br.com.zup.edu.shared.grpc.ExceptionHandler.* | |
import io.grpc.Status | |
import javax.inject.Singleton | |
@Singleton | |
class ChavePixNaoEncontradaExceptionHandler : ExceptionHandler<ChavePixNaoEncontradaException> { | |
override fun handle(e: ChavePixNaoEncontradaException): StatusWithDetails { | |
return StatusWithDetails(Status.NOT_FOUND | |
.withDescription(e.message) | |
.withCause(e)) | |
} | |
override fun supports(e: Exception): Boolean { | |
return e is ChavePixNaoEncontradaException | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.zup.edu.shared.grpc.handlers | |
import br.com.zup.edu.pix.ChavePixExistenteException | |
import br.com.zup.edu.shared.grpc.ExceptionHandler | |
import br.com.zup.edu.shared.grpc.ExceptionHandler.StatusWithDetails | |
import io.grpc.Status | |
import javax.inject.Singleton | |
@Singleton | |
class ChavePixExistenteExceptionHandler : ExceptionHandler<ChavePixExistenteException> { | |
override fun handle(e: ChavePixExistenteException): StatusWithDetails { | |
return StatusWithDetails(Status.ALREADY_EXISTS | |
.withDescription(e.message) | |
.withCause(e)) | |
} | |
override fun supports(e: Exception): Boolean { | |
return e is ChavePixExistenteException | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package br.com.zup.edu.conf | |
import io.micronaut.context.MessageSource | |
import io.micronaut.context.annotation.Bean | |
import io.micronaut.context.annotation.Factory | |
import io.micronaut.context.i18n.ResourceBundleMessageSource | |
import io.micronaut.runtime.context.CompositeMessageSource | |
import javax.inject.Singleton | |
@Factory | |
class I18nConfig { | |
@Bean | |
@Singleton | |
fun messageSource(): MessageSource { | |
return CompositeMessageSource(listOf( | |
ResourceBundleMessageSource("messages") // messages.properties | |
)) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
data.integrity.error.uk_author_email=author already exists |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
@Table(uniqueConstraints = [UniqueConstraint( | |
name = "uk_author_email", // you must define the constraint name properly | |
columnNames = ["email"] | |
)]) | |
class Author( | |
// other fields | |
@Column(unique = true, nullable = false) | |
val email: String, | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here is an alternative using AOP interceptor instead, this way we don't need to worry about the gRPC life-cycle