CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-quarkus--quarkus-grpc

Quarkus gRPC extension that enables implementing and consuming gRPC services with reactive and imperative programming models.

Pending
Overview
Eval results
Files

exception-handling.mddocs/

Exception Handling

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.

Capabilities

ExceptionHandler Abstract Class

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);
    }
}

ExceptionHandlerProvider Interface

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);
    }
}

Status Utility Methods

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;
        }
    }
}

Common Exception Handling Patterns

Business Logic Exception Mapping

@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;
    }
}

Structured Error Response

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();
    }
}

Reactive Exception Handling

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);
    }
}

Exception Handling Best Practices

  1. Map business exceptions to appropriate gRPC status codes
  2. Preserve error context in status descriptions
  3. Use metadata for structured error information
  4. Log exceptions appropriately (don't expose sensitive details)
  5. Provide correlation IDs for error tracking
  6. Use reactive error handling patterns in Mutiny services
  7. Consider circuit breaker patterns for external service failures

Install with Tessl CLI

npx tessl i tessl/maven-io-quarkus--quarkus-grpc

docs

client-usage.md

configuration.md

exception-handling.md

index.md

interceptors.md

reactive-streaming.md

service-implementation.md

tile.json