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?
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
}),