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

interceptors.mddocs/

Interceptors

Interceptor system for implementing cross-cutting concerns like authentication, logging, and metrics across gRPC services and clients. Interceptors can be registered globally or for specific services/clients.

Capabilities

@GlobalInterceptor Annotation

Denotes an interceptor that should be registered for all gRPC services or all injected gRPC clients. The interceptor will be automatically applied without explicit registration.

/**
 * Denotes a {@link io.grpc.ServerInterceptor} that should be registered for all gRPC services, or a
 * {@link io.grpc.ClientInterceptor} that should be registered for all injected gRPC clients.
 *
 * @see RegisterInterceptor
 * @see RegisterClientInterceptor
 */
@Target({ FIELD, PARAMETER, TYPE, METHOD })
@Retention(RUNTIME)
public @interface GlobalInterceptor {
}

Usage Examples:

import io.quarkus.grpc.GlobalInterceptor;
import io.grpc.ServerInterceptor;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import jakarta.enterprise.context.ApplicationScoped;

// Global server interceptor
@GlobalInterceptor
@ApplicationScoped
public class LoggingServerInterceptor implements ServerInterceptor {
    
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        
        String methodName = call.getMethodDescriptor().getFullMethodName();
        System.out.println("Received call to: " + methodName);
        
        return next.startCall(call, headers);
    }
}

// Global client interceptor
@GlobalInterceptor
@ApplicationScoped
public class AuthClientInterceptor implements ClientInterceptor {
    
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
            MethodDescriptor<ReqT, RespT> method,
            CallOptions callOptions,
            Channel next) {
        
        return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
                next.newCall(method, callOptions)) {
            
            @Override
            public void start(Listener<RespT> responseListener, Metadata headers) {
                // Add auth token to all outgoing calls
                Key<String> authKey = Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
                headers.put(authKey, "Bearer " + getAuthToken());
                super.start(responseListener, headers);
            }
        };
    }
}

@RegisterInterceptor Annotation

Registers a ServerInterceptor for a particular gRPC service. This annotation is repeatable, allowing multiple interceptors to be registered for the same service.

/**
 * Registers a {@link ServerInterceptor} for a particular gRPC service.
 *
 * @see GlobalInterceptor
 */
@Repeatable(RegisterInterceptors.class)
@Target(TYPE)
@Retention(RUNTIME)
public @interface RegisterInterceptor {
    Class<? extends ServerInterceptor> value();
}

/**
 * Container annotation for repeatable {@link RegisterInterceptor} annotations.
 */
@Target(TYPE)
@Retention(RUNTIME)
@Documented
public @interface RegisterInterceptors {
    RegisterInterceptor[] value();
}

Usage Examples:

import io.quarkus.grpc.RegisterInterceptor;
import io.quarkus.grpc.GrpcService;

// Single interceptor
@RegisterInterceptor(AuthInterceptor.class)
@GrpcService
public class SecureService implements MutinyService {
    
    public Uni<SecureResponse> secureMethod(SecureRequest request) {
        // This method will be intercepted by AuthInterceptor
        return Uni.createFrom().item(SecureResponse.newBuilder().build());
    }
}

// Multiple interceptors
@RegisterInterceptor(LoggingInterceptor.class)
@RegisterInterceptor(MetricsInterceptor.class)
@RegisterInterceptor(AuthInterceptor.class)
@GrpcService
public class FullyInterceptedService implements MutinyService {
    
    public Uni<Response> method(Request request) {
        // Interceptors will be applied in registration order
        return Uni.createFrom().item(Response.newBuilder().build());
    }
}

@RegisterClientInterceptor Annotation

Registers a ClientInterceptor for an injected gRPC client. This annotation is repeatable and supports programmatic creation.

/**
 * Registers a {@link ClientInterceptor} for an injected gRPC client.
 *
 * @see GlobalInterceptor
 */
@Qualifier
@Repeatable(RegisterClientInterceptor.List.class)
@Target({ FIELD, PARAMETER })
@Retention(RUNTIME)
public @interface RegisterClientInterceptor {
    
    Class<? extends ClientInterceptor> value();
    
    @Target({ FIELD, PARAMETER })
    @Retention(RUNTIME)
    @interface List {
        RegisterClientInterceptor[] value();
    }
    
    final class Literal extends AnnotationLiteral<RegisterClientInterceptor> 
            implements RegisterClientInterceptor {
        private static final long serialVersionUID = 1L;
        private final Class<? extends ClientInterceptor> value;
        
        public static Literal of(Class<? extends ClientInterceptor> value);
        
        public Class<? extends ClientInterceptor> value();
    }
}

Usage Examples:

import io.quarkus.grpc.RegisterClientInterceptor;
import io.quarkus.grpc.GrpcClient;

public class ClientWithInterceptors {
    
    // Single client interceptor
    @Inject
    @GrpcClient("user-service")
    @RegisterClientInterceptor(RetryInterceptor.class)
    MutinyUserServiceGrpc userClient;
    
    // Multiple client interceptors
    @Inject
    @GrpcClient("payment-service")
    @RegisterClientInterceptor(AuthInterceptor.class)
    @RegisterClientInterceptor(LoggingInterceptor.class)
    MutinyPaymentServiceGrpc paymentClient;
    
    // Programmatic interceptor registration
    public void registerInterceptorProgrammatically() {
        var literal = RegisterClientInterceptor.Literal.of(CustomInterceptor.class);
        // Use literal in CDI lookup
    }
}

Common Interceptor Implementations

Authentication Interceptor

@ApplicationScoped
public class AuthInterceptor implements ServerInterceptor {
    
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        
        Key<String> authKey = Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
        String token = headers.get(authKey);
        
        if (token == null || !isValidToken(token)) {
            call.close(Status.UNAUTHENTICATED.withDescription("Invalid or missing auth token"), 
                      new Metadata());
            return new ServerCall.Listener<ReqT>() {};
        }
        
        return next.startCall(call, headers);
    }
    
    private boolean isValidToken(String token) {
        return token.startsWith("Bearer ") && validateJwtToken(token.substring(7));
    }
}

Logging Interceptor

@ApplicationScoped
public class LoggingInterceptor implements ServerInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);
    
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        
        String methodName = call.getMethodDescriptor().getFullMethodName();
        long startTime = System.currentTimeMillis();
        
        logger.info("Starting gRPC call: {}", methodName);
        
        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(
                next.startCall(call, headers)) {
            
            @Override
            public void onComplete() {
                long duration = System.currentTimeMillis() - startTime;
                logger.info("Completed gRPC call: {} in {}ms", methodName, duration);
                super.onComplete();
            }
            
            @Override
            public void onCancel() {
                logger.warn("Cancelled gRPC call: {}", methodName);
                super.onCancel();
            }
        };
    }
}

Metrics Interceptor

@ApplicationScoped
public class MetricsInterceptor implements ServerInterceptor {
    
    @Inject
    MeterRegistry meterRegistry;
    
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        
        String methodName = call.getMethodDescriptor().getFullMethodName();
        Timer.Sample sample = Timer.start(meterRegistry);
        
        Counter.builder("grpc.server.requests")
            .tag("method", methodName)
            .register(meterRegistry)
            .increment();
        
        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(
                next.startCall(call, headers)) {
            
            @Override
            public void onComplete() {
                sample.stop(Timer.builder("grpc.server.duration")
                    .tag("method", methodName)
                    .tag("status", "completed")
                    .register(meterRegistry));
                super.onComplete();
            }
            
            @Override
            public void onCancel() {
                sample.stop(Timer.builder("grpc.server.duration")
                    .tag("method", methodName)
                    .tag("status", "cancelled")
                    .register(meterRegistry));
                super.onCancel();
            }
        };
    }
}

Client Retry Interceptor

@ApplicationScoped
public class RetryInterceptor implements ClientInterceptor {
    
    private final int maxRetries = 3;
    private final long retryDelayMs = 1000;
    
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
            MethodDescriptor<ReqT, RespT> method,
            CallOptions callOptions,
            Channel next) {
        
        return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
                next.newCall(method, callOptions)) {
            
            @Override
            public void start(Listener<RespT> responseListener, Metadata headers) {
                super.start(new RetryingListener<>(responseListener, method, callOptions, next),
                           headers);
            }
        };
    }
    
    private class RetryingListener<RespT> extends 
            ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT> {
        
        private int attemptCount = 0;
        
        protected RetryingListener(Listener<RespT> delegate, 
                                 MethodDescriptor<?, RespT> method,
                                 CallOptions callOptions, 
                                 Channel channel) {
            super(delegate);
            // Store for retry logic
        }
        
        @Override
        public void onClose(Status status, Metadata trailers) {
            if (status.isOk() || attemptCount >= maxRetries) {
                super.onClose(status, trailers);
            } else {
                attemptCount++;
                // Implement retry logic
                scheduleRetry();
            }
        }
    }
}

Interceptor Ordering

  • Global interceptors are applied first
  • Service-specific interceptors (@RegisterInterceptor) are applied in registration order
  • Client-specific interceptors (@RegisterClientInterceptor) are applied in registration order
  • Interceptors form a chain where each interceptor can modify the request/response or short-circuit the call

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