自定义gRPC Gateway拦截器

本文通过实现一个从请求Header中获取自定义字段,将其注入context中的拦截器,介绍如何实现一个自定义的gRPC Gateway拦截器。

grpc.UnaryServerInterceptor

grpc标准包中定义了grpc拦截器的格式,如下:

// UnaryServerInterceptor provides a hook to intercept the execution of a unary RPC on the server. info
// contains all the information of this RPC the interceptor can operate on. And handler is the wrapper
// of the service method implementation. It is the responsibility of the interceptor to invoke handler
// to complete the RPC.
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

实现的服务接口中ctx从interceptor ctx继承而来,req为接口Request载体,info承载了servermethod信息,handler定义了interceptor的处理程序。

info定义如下:

// UnaryServerInfo consists of various information about a unary RPC on
// server side. All per-rpc information may be mutated by the interceptor.
type UnaryServerInfo struct {
    // Server is the service implementation the user provides. This is read-only.
    Server interface{}
    // FullMethod is the full RPC method string, i.e., /package.service/method.
    FullMethod string
}

handler定义如下:

// UnaryHandler defines the handler invoked by UnaryServerInterceptor to complete the normal
// execution of a unary RPC. If a UnaryHandler returns an error, it should be produced by the
// status package, or else gRPC will use codes.Unknown as the status code and err.Error() as
// the status message of the RPC.
type UnaryHandler func(ctx context.Context, req interface{}) (interface{}, error)

自定义interceptor

了解interceptor结构后,就可根据业务功能自定义实现interceptor。下文为一个从header中读取自定义字段,将其值赋给新加字段,从填充到context中的拦截器实现。

// UnaryServerInterceptor customized unary server interceptor
// duplicates a new header field using old one, add it to context and return
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
    return func(
        ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
    ) (resp interface{}, err error) {
        if md, ok := metadata.FromIncomingContext(ctx); ok {
            var value []string
            if v, ok := md[string(OldKey)]; ok {
                value = v
            }
            md1 := metadata.Pairs(string(NewKey), value[0])
            md2 := metadata.Join(md, md1)
            ctx = metadata.NewIncomingContext(ctx, md2)
        }

        return handler(ctx, req)
    }
}

涉及到的相关函数可通过go pkg查看。

接口从context获取自定义值

UnaryServerInterceptor函数以gRPC metadata结构往context中添加新的字段,起类型为metadata包定义的mdIncomingKey类型,此类型不可导出。

type mdIncomingKey struct{}

// NewIncomingContext creates a new context with incoming md attached.
func NewIncomingContext(ctx context.Context, md MD) context.Context {
    return context.WithValue(ctx, mdIncomingKey{}, md)
}

所以新添加的字段是无法以string等基本类型直接获取,好的做法是实现统一的提取函数,如下:

// ExtractMetadata extract NewKey from context and return
func ExtractMetadata(ctx context.Context, key KeyType) (interface{}, bool) {
    if md, ok := metadata.FromIncomingContext(ctx); ok {
        v := md.Get(string(key))
        if len(v) > 0 {
            return v[0], true
        }

        return nil, true
    }
    return nil, false
}

完整代码

完整代码可参见grpc-helloworld

效果

img.png

img.png

Tips

当使用gRPC Gateway提供的restful服务时,Header中的自定义字段需以Grpc-metadata-作为前缀,否则gRPC Gateway网关无法识别。参见github issue

参考

customizing header issue

来瞧一瞧 gRPC的拦截器