Repository: grpc-ecosystem/go-grpc-middleware
Pull request #684 (2023-12-20 16:26:20)

protovalidate: add option to ignore certain message types
## Changes

Add an option to ignore certain types of messages that are not ready to be validated at the middleware level and require some further processing and manual `protovalidate.Validate()` call.

## Verification

Unit test to verify that bad message can be ignored because of the option implemented.


igor-tsiglyar: > Could you add an example_test.go file that shows how to use this new option? Thanks!

@johanbrandhorst adjusted the existing interceptors/protovalidate/example_unary_test.go as I didn't see any usages of it, figured it is only for demonstrational purposes

also, handled the option in a stream handler for consistency 

Patch:
diff --git a/interceptors/protovalidate/options.go b/interceptors/protovalidate/options.go
new file mode 100644
index 0000000..764b55e
--- /dev/null
+++ b/interceptors/protovalidate/options.go
@@ -0,0 +1,41 @@
+// Copyright (c) The go-grpc-middleware Authors.
+// Licensed under the Apache License 2.0.
+
+// Copyright 2017 David Ackroyd. All Rights Reserved.
+// See LICENSE for licensing terms.
+
+package protovalidate
+
+import (
+	"golang.org/x/exp/slices"
+	"google.golang.org/protobuf/reflect/protoreflect"
+)
+
+type options struct {
+	ignoreMessages []protoreflect.MessageType
+}
+
+// An Option lets you add options to protovalidate interceptors using With* funcs.
+type Option func(*options)
+
+func evaluateOpts(opts []Option) *options {
+	optCopy := &options{}
+	for _, o := range opts {
+		o(optCopy)
+	}
+	return optCopy
+}
+
+// WithIgnoreMessages sets the messages that should be ignored by the validator. Use with
+// caution and ensure validation is performed elsewhere.
+func WithIgnoreMessages(msgs ...protoreflect.MessageType) Option {
+	return func(o *options) {
+		o.ignoreMessages = msgs
+	}
+}
+
+func (o *options) shouldIgnoreMessage(m protoreflect.MessageType) bool {
+	return slices.ContainsFunc(o.ignoreMessages, func(t protoreflect.MessageType) bool {
+		return m == t
+	})
+}
diff --git a/interceptors/protovalidate/protovalidate.go b/interceptors/protovalidate/protovalidate.go
index 426fb50..cf337db 100644
--- a/interceptors/protovalidate/protovalidate.go
+++ b/interceptors/protovalidate/protovalidate.go
@@ -14,12 +14,6 @@ import (
 	"google.golang.org/protobuf/proto"
 )
 
-// Option interface is currently empty and serves as a placeholder for potential future implementations.
-// It allows adding new options without breaking existing code.
-type Option interface {
-	unimplemented()
-}
-
 // UnaryServerInterceptor returns a new unary server interceptor that validates incoming messages.
 func UnaryServerInterceptor(validator *protovalidate.Validator, opts ...Option) grpc.UnaryServerInterceptor {
 	return func(
@@ -28,8 +22,12 @@ func UnaryServerInterceptor(validator *protovalidate.Validator, opts ...Option)
 		info *grpc.UnaryServerInfo,
 		handler grpc.UnaryHandler,
 	) (resp interface{}, err error) {
+		o := evaluateOpts(opts)
 		switch msg := req.(type) {
 		case proto.Message:
+			if o.shouldIgnoreMessage(msg.ProtoReflect().Type()) {
+				break
+			}
 			if err = validator.Validate(msg); err != nil {
 				return nil, status.Error(codes.InvalidArgument, err.Error())
 			}
@@ -54,6 +52,7 @@ func StreamServerInterceptor(validator *protovalidate.Validator, opts ...Option)
 		wrapped := wrapServerStream(stream)
 		wrapped.wrappedContext = ctx
 		wrapped.validator = validator
+		wrapped.options = evaluateOpts(opts)
 
 		return handler(srv, wrapped)
 	}
@@ -64,7 +63,11 @@ func (w *wrappedServerStream) RecvMsg(m interface{}) error {
 		return err
 	}
 
-	if err := w.validator.Validate(m.(proto.Message)); err != nil {
+	msg := m.(proto.Message)
+	if w.options.shouldIgnoreMessage(msg.ProtoReflect().Type()) {
+		return nil
+	}
+	if err := w.validator.Validate(msg); err != nil {
 		return status.Error(codes.InvalidArgument, err.Error())
 	}
 
@@ -78,6 +81,7 @@ type wrappedServerStream struct {
 	wrappedContext context.Context
 
 	validator *protovalidate.Validator
+	options   *options
 }
 
 // Context returns the wrapper's WrappedContext, overwriting the nested grpc.ServerStream.Context()


Test patch:
diff --git a/interceptors/protovalidate/example_stream_test.go b/interceptors/protovalidate/example_stream_test.go
index 2513df9..01ee2b9 100644
--- a/interceptors/protovalidate/example_stream_test.go
+++ b/interceptors/protovalidate/example_stream_test.go
@@ -29,7 +29,10 @@ func ExampleStreamServerInterceptor() {
 	var (
 		srv = grpc.NewServer(
 			grpc.StreamInterceptor(
-				protovalidate_middleware.StreamServerInterceptor(validator),
+				protovalidate_middleware.StreamServerInterceptor(validator,
+					protovalidate_middleware.WithIgnoreMessages(
+						(&testvalidatev1.SendStreamRequest{}).ProtoReflect().Type(),
+					)),
 			),
 		)
 		svc = &StreamService{}
diff --git a/interceptors/protovalidate/example_unary_test.go b/interceptors/protovalidate/example_unary_test.go
index 0e9f2de..a6fe75d 100644
--- a/interceptors/protovalidate/example_unary_test.go
+++ b/interceptors/protovalidate/example_unary_test.go
@@ -30,7 +30,11 @@ func ExampleUnaryServerInterceptor() {
 	var (
 		srv = grpc.NewServer(
 			grpc.UnaryInterceptor(
-				protovalidate_middleware.UnaryServerInterceptor(validator),
+				protovalidate_middleware.UnaryServerInterceptor(validator,
+					protovalidate_middleware.WithIgnoreMessages(
+						(&testvalidatev1.SendRequest{}).ProtoReflect().Type(),
+					),
+				),
 			),
 		)
 		svc = &UnaryService{}
diff --git a/interceptors/protovalidate/protovalidate_test.go b/interceptors/protovalidate/protovalidate_test.go
index f5f0c58..f626f39 100644
--- a/interceptors/protovalidate/protovalidate_test.go
+++ b/interceptors/protovalidate/protovalidate_test.go
@@ -19,6 +19,7 @@ import (
 	"google.golang.org/grpc/credentials/insecure"
 	"google.golang.org/grpc/status"
 	"google.golang.org/grpc/test/bufconn"
+	"google.golang.org/protobuf/reflect/protoreflect"
 )
 
 func TestUnaryServerInterceptor(t *testing.T) {
@@ -50,6 +51,20 @@ func TestUnaryServerInterceptor(t *testing.T) {
 		assert.Error(t, err)
 		assert.Equal(t, codes.InvalidArgument, status.Code(err))
 	})
+
+	interceptor = protovalidate_middleware.UnaryServerInterceptor(validator,
+		protovalidate_middleware.WithIgnoreMessages(testvalidate.BadUnaryRequest.ProtoReflect().Type()),
+	)
+
+	t.Run("invalid_email_ignored", func(t *testing.T) {
+		info := &grpc.UnaryServerInfo{
+			FullMethod: "FakeMethod",
+		}
+
+		resp, err := interceptor(context.TODO(), testvalidate.BadUnaryRequest, info, handler)
+		assert.Nil(t, err)
+		assert.Equal(t, resp, "good")
+	})
 }
 
 type server struct {
@@ -69,7 +84,7 @@ func (g *server) SendStream(
 
 const bufSize = 1024 * 1024
 
-func startGrpcServer(t *testing.T) *grpc.ClientConn {
+func startGrpcServer(t *testing.T, ignoreMessages ...protoreflect.MessageType) *grpc.ClientConn {
 	lis := bufconn.Listen(bufSize)
 
 	validator, err := protovalidate.New()
@@ -77,7 +92,9 @@ func startGrpcServer(t *testing.T) *grpc.ClientConn {
 
 	s := grpc.NewServer(
 		grpc.StreamInterceptor(
-			protovalidate_middleware.StreamServerInterceptor(validator),
+			protovalidate_middleware.StreamServerInterceptor(validator,
+				protovalidate_middleware.WithIgnoreMessages(ignoreMessages...),
+			),
 		),
 	)
 	testvalidatev1.RegisterTestValidateServiceServer(s, &server{})
@@ -131,4 +148,16 @@ func TestStreamServerInterceptor(t *testing.T) {
 		assert.Error(t, err)
 		assert.Equal(t, codes.InvalidArgument, status.Code(err))
 	})
+
+	t.Run("invalid_email_ignored", func(t *testing.T) {
+		client := testvalidatev1.NewTestValidateServiceClient(
+			startGrpcServer(t, testvalidate.BadStreamRequest.ProtoReflect().Type()),
+		)
+
+		out, err := client.SendStream(context.Background(), testvalidate.BadStreamRequest)
+		assert.Nil(t, err)
+
+		_, err = out.Recv()
+		assert.Nil(t, err)
+	})
 }