Quarkus gRPC extension that enables implementing and consuming gRPC services with reactive and imperative programming models.
—
Comprehensive exception handling system with customizable error transformation and gRPC status code mapping. The system provides hooks for converting application exceptions into proper gRPC status responses.
Generic exception handler that intercepts exceptions during gRPC call processing and provides customization points for error transformation.
/**
* Generic exception handler
*/
public abstract class ExceptionHandler<ReqT, RespT> extends
ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT> {
private final ServerCall<ReqT, RespT> call;
private final Metadata metadata;
public ExceptionHandler(ServerCall.Listener<ReqT> listener,
ServerCall<ReqT, RespT> call,
Metadata metadata) {
super(listener);
this.metadata = metadata;
this.call = call;
}
protected abstract void handleException(Throwable t,
ServerCall<ReqT, RespT> call,
Metadata metadata);
}Usage Example:
import io.quarkus.grpc.ExceptionHandler;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
public class CustomExceptionHandler<ReqT, RespT> extends ExceptionHandler<ReqT, RespT> {
public CustomExceptionHandler(ServerCall.Listener<ReqT> listener,
ServerCall<ReqT, RespT> call,
Metadata metadata) {
super(listener, call, metadata);
}
@Override
protected void handleException(Throwable t,
ServerCall<ReqT, RespT> call,
Metadata metadata) {
Status status;
if (t instanceof ValidationException) {
status = Status.INVALID_ARGUMENT.withDescription(t.getMessage());
} else if (t instanceof NotFoundException) {
status = Status.NOT_FOUND.withDescription(t.getMessage());
} else if (t instanceof UnauthorizedException) {
status = Status.UNAUTHENTICATED.withDescription(t.getMessage());
} else if (t instanceof ForbiddenException) {
status = Status.PERMISSION_DENIED.withDescription(t.getMessage());
} else {
status = Status.INTERNAL.withDescription("Internal server error");
}
call.close(status, metadata);
}
}Provider for creating ExceptionHandler instances. Implement this interface and expose it as a CDI bean to customize exception handling across all gRPC services.
/**
* Provider for ExceptionHandler.
*
* To use a custom ExceptionHandler, extend {@link ExceptionHandler} and implement
* an {@link ExceptionHandlerProvider}, and expose it as a CDI bean.
*/
public interface ExceptionHandlerProvider {
<ReqT, RespT> ExceptionHandler<ReqT, RespT> createHandler(
Listener<ReqT> listener,
ServerCall<ReqT, RespT> serverCall,
Metadata metadata
);
default Throwable transform(Throwable t) {
return toStatusException(t, false); // previous default was false
}
/**
* Throw Status exception.
*
* @param t the throwable to transform
* @param runtime true if we should throw StatusRuntimeException, false for StatusException
* @return Status(Runtime)Exception
*/
static Exception toStatusException(Throwable t, boolean runtime);
/**
* Get Status from exception.
*
* @param t the throwable to read or create status from
* @return gRPC Status instance
*/
static Status toStatus(Throwable t);
/**
* Get optional Metadata from exception.
*
* @param t the throwable to read or create metadata from
* @return optional gRPC Metadata instance
*/
static Optional<Metadata> toTrailers(Throwable t);
}Usage Examples:
import io.quarkus.grpc.ExceptionHandlerProvider;
import io.quarkus.grpc.ExceptionHandler;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CustomExceptionHandlerProvider implements ExceptionHandlerProvider {
@Override
public <ReqT, RespT> ExceptionHandler<ReqT, RespT> createHandler(
Listener<ReqT> listener,
ServerCall<ReqT, RespT> serverCall,
Metadata metadata) {
return new CustomExceptionHandler<>(listener, serverCall, metadata);
}
@Override
public Throwable transform(Throwable t) {
// Custom transformation logic
if (t instanceof BusinessException) {
BusinessException be = (BusinessException) t;
Status status = Status.INVALID_ARGUMENT
.withDescription(be.getMessage())
.withCause(be);
return status.asRuntimeException();
}
// Use default transformation for other exceptions
return ExceptionHandlerProvider.toStatusException(t, true);
}
}The ExceptionHandlerProvider interface provides static utility methods for exception transformation:
/**
* Throw Status exception.
*/
static Exception toStatusException(Throwable t, boolean runtime) {
// Converts any throwable to StatusException or StatusRuntimeException
}
/**
* Get Status from exception.
*/
static Status toStatus(Throwable t) {
// Extracts or creates gRPC Status from throwable
}
/**
* Get optional Metadata from exception.
*/
static Optional<Metadata> toTrailers(Throwable t) {
// Extracts optional Metadata trailers from exception
}Usage Examples:
import io.quarkus.grpc.ExceptionHandlerProvider;
import io.grpc.Status;
import io.grpc.Metadata;
public class ExceptionUtils {
public static void demonstrateStatusConversion() {
try {
riskyOperation();
} catch (Exception e) {
// Convert to gRPC status
Status status = ExceptionHandlerProvider.toStatus(e);
System.out.println("Status code: " + status.getCode());
System.out.println("Description: " + status.getDescription());
// Get trailers if available
Optional<Metadata> trailers = ExceptionHandlerProvider.toTrailers(e);
if (trailers.isPresent()) {
System.out.println("Has trailers: " + trailers.get().keys());
}
// Convert to StatusRuntimeException
Exception statusException = ExceptionHandlerProvider.toStatusException(e, true);
throw (StatusRuntimeException) statusException;
}
}
}@ApplicationScoped
public class BusinessExceptionHandler implements ExceptionHandlerProvider {
@Override
public <ReqT, RespT> ExceptionHandler<ReqT, RespT> createHandler(
Listener<ReqT> listener,
ServerCall<ReqT, RespT> serverCall,
Metadata metadata) {
return new ExceptionHandler<ReqT, RespT>(listener, serverCall, metadata) {
@Override
protected void handleException(Throwable t,
ServerCall<ReqT, RespT> call,
Metadata metadata) {
Status status = mapToGrpcStatus(t);
Metadata responseMetadata = createResponseMetadata(t, metadata);
call.close(status, responseMetadata);
}
};
}
private Status mapToGrpcStatus(Throwable t) {
if (t instanceof ValidationException) {
ValidationException ve = (ValidationException) t;
return Status.INVALID_ARGUMENT
.withDescription("Validation failed: " + ve.getMessage())
.withCause(ve);
}
if (t instanceof ResourceNotFoundException) {
return Status.NOT_FOUND
.withDescription("Resource not found: " + t.getMessage());
}
if (t instanceof InsufficientPermissionsException) {
return Status.PERMISSION_DENIED
.withDescription("Access denied: " + t.getMessage());
}
if (t instanceof RateLimitExceededException) {
return Status.RESOURCE_EXHAUSTED
.withDescription("Rate limit exceeded");
}
// Default for unexpected exceptions
return Status.INTERNAL
.withDescription("Internal server error")
.withCause(t);
}
private Metadata createResponseMetadata(Throwable t, Metadata requestMetadata) {
Metadata responseMetadata = new Metadata();
// Add correlation ID if present in request
Key<String> correlationKey = Key.of("correlation-id", Metadata.ASCII_STRING_MARSHALLER);
String correlationId = requestMetadata.get(correlationKey);
if (correlationId != null) {
responseMetadata.put(correlationKey, correlationId);
}
// Add error details for certain exception types
if (t instanceof ValidationException) {
Key<String> errorDetailsKey = Key.of("error-details", Metadata.ASCII_STRING_MARSHALLER);
ValidationException ve = (ValidationException) t;
responseMetadata.put(errorDetailsKey, ve.getValidationErrors().toString());
}
return responseMetadata;
}
}public class StructuredErrorHandler implements ExceptionHandlerProvider {
@Override
public Throwable transform(Throwable t) {
ErrorInfo errorInfo = createErrorInfo(t);
Status status = Status.fromCode(errorInfo.getStatusCode())
.withDescription(errorInfo.getMessage());
Metadata metadata = new Metadata();
Key<String> errorCodeKey = Key.of("error-code", Metadata.ASCII_STRING_MARSHALLER);
metadata.put(errorCodeKey, errorInfo.getErrorCode());
if (errorInfo.hasDetails()) {
Key<String> detailsKey = Key.of("error-details", Metadata.ASCII_STRING_MARSHALLER);
metadata.put(detailsKey, errorInfo.getDetailsJson());
}
return status.asRuntimeException(metadata);
}
private ErrorInfo createErrorInfo(Throwable t) {
if (t instanceof ValidationException) {
ValidationException ve = (ValidationException) t;
return ErrorInfo.builder()
.statusCode(Status.Code.INVALID_ARGUMENT)
.errorCode("VALIDATION_FAILED")
.message(ve.getMessage())
.details(ve.getValidationErrors())
.build();
}
if (t instanceof ResourceNotFoundException) {
return ErrorInfo.builder()
.statusCode(Status.Code.NOT_FOUND)
.errorCode("RESOURCE_NOT_FOUND")
.message(t.getMessage())
.build();
}
// Default error info
return ErrorInfo.builder()
.statusCode(Status.Code.INTERNAL)
.errorCode("INTERNAL_ERROR")
.message("An unexpected error occurred")
.build();
}
}In gRPC services that use Mutiny, exceptions can be handled reactively:
@GrpcService
public class ReactiveServiceWithErrorHandling implements MutinyService {
public Uni<UserResponse> getUser(UserRequest request) {
return validateRequest(request)
.onItem().transformToUni(this::findUser)
.onFailure(ValidationException.class).transform(ex ->
new StatusRuntimeException(
Status.INVALID_ARGUMENT.withDescription(ex.getMessage())))
.onFailure(UserNotFoundException.class).transform(ex ->
new StatusRuntimeException(
Status.NOT_FOUND.withDescription("User not found")))
.onFailure().transform(ex -> {
if (ex instanceof StatusRuntimeException) {
return ex; // Already a gRPC exception
}
return new StatusRuntimeException(
Status.INTERNAL.withDescription("Unexpected error"));
});
}
private Uni<UserRequest> validateRequest(UserRequest request) {
if (request.getUserId().isEmpty()) {
return Uni.createFrom().failure(
new ValidationException("User ID is required"));
}
return Uni.createFrom().item(request);
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-quarkus--quarkus-grpc