gointerceptorgrpc-go

How to use the `reply` parameter in the gRPC UnaryClientInterceptor?


The UnaryClientInterceptor interface from google.golang.org/grpc has a signature like

type UnaryClientInterceptor func(ctx context.Context, method string, req, reply any, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

My understanding of this interface is that implementations are called before the request is made, allowing for the enrichment and modification of the request before it hits the network.

I don't, however, understand the reply parameter since the request hasn't been made yet. Am I misunderstanding the use of this interface or are there interesting things that can be done with the reply parameter even given that we can't actually access the response?


Solution

  • You can inspect the response within the reply argument after the gRPC call is made.

    One of the arguments of the unary interceptor is the invoker function, which has type UnaryInvoker. Within the unary interceptor, you are supposed to call this function, as mentioned in the docs:

    invoker is the handler to complete the RPC and it is the responsibility of the interceptor to call it

    And invoker itself takes req and reply as arguments. You can call the invoker and then inspect reply.

    But, how to inspect it? The dynamic type of the reply interface is supposed to be a pointer to the return type of your gRPC handler as defined in your protobuffer schema. You can inspect the value of method to know what return type to expect.

    To understand this better, you can take a look at the code generated by protoc. The implementation of a gRPC method shows what are the actual arguments used to call Invoke.

    Let's say you have a protobuffer like:

    package foo;
    
    service MyService {
        rpc GetFoo(FooRequest) returns (FooResponse);
    }
    

    The generated code for the GetFoo method will look like:

    func (c *myServiceClient) GetFoo(ctx context.Context, in *FooRequest, opts ...grpc.CallOption) (*FooResponse, error) {
        out := new(FooResponse)
        err := c.cc.Invoke(ctx, "/foo.MyService/GetFoo", in, out, opts...)
        if err != nil {
            return nil, err
        }
        return out, nil
    }
    

    Therefore you know the type of reply based on the value of method. From then, you can obtain the following example unary interceptor:

    grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
        // some interceptor logic
        // ...
        // then you must call invoker
        err := invoker(ctx, method, req, reply, cc, opts...)
        if err != nil {
            // handle error
        }
        // finally you can inspect reply before returning
        switch method {
        case "/foo.MyService/GetFoo":
            fooResp := reply.(*FooResponse)
            // access fields to do whatever you want
    
        // other switch cases as needed
        }
        return nil
    }),