Всем привет) Те кому нравится использовать GRPC скорее всего с этой библиотекой уже знакомы. Это protoc plugin который читает *.proto файлы и генерит обратный прокси сервер который принимает HTTP и транслирует их в GRPC. Довольно полезная штука, когда у нас есть сервер к которому можно ходить как по GRPC так и HTTP.
Но при использовании данного плагина я понял что невозможно нормально использовать middleware. Самым проблемным в этом плане оказалась JWT авторизация, ибо я хотел бы валидировать JWT токен, и после этого ID юзера запихивать в context запроса, чтобы на каждом шаге запроса знать кто именно делает данный запрос)
При создании роутера c помощью grpc-gateway есть возможность задать некоторые опции, выглядит это примерно так:
import "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
g := runtime.NewServeMux(
runtime.WithMetadata(),
runtime.WithForwardResponseOption(response.HTTPResponseModifier),
)
В опции сюда можно прокинуть довольно много всего, но нет возможности прокинуть middleware, которая может модифицировать context и при этом может вернуть ошибку.
После создания роутера надо зарегистрировать все методы которые объявлены в .proto файле. Для этого используется сгенерированная с помощью grpc-gateway функция. Пример использования этого метода:
err = sdk.RegisterTestHandlerServer(ctx, g, handlers.grpcHandler)
if err != nil {
return nil, fmt.Errorf("error while initing handlers %w", err)
}
Я решил модифицировать код, который генерирует grpc-gateway, для того чтобы иметь возможность прокинуть middleware внутрь функции RegisterTestHandlerServer.
Middleware у меня будет иметь следующий интерфейс:
func(
ctx context.Context, // тут можно модифицировать контекст
req interface{},
info *UnaryServerInfo, // отсюда мы можем узнать название вызванного метода
handler UnaryHandler,
)
(resp interface{}, err error)
Те кто шарят уже наверное поняли что это интерфейс UnaryServerInterceptor. Я решил использовать этот интерфейс по причине того, что его можно будет юзать как для GRPC запросов, так и для HTTP запросов. То есть можно написать один middleware и использовать его как для GRPC так и для HTTP вызовов.
Для того чтобы вносить изменения в код который сгенерировал grpc-gateway я решил написать protoc плагин, который будет проходиться по *.pb.gw.go файлам(это файлы которые генерит grpc-gateway) и вносить изменения в функцию Register<ServiceName>HandlerServer
Нам надо добавить тип UnaryServerInterceptor в аргументы функции.
Далее нам надо где-то до вызова самого метода вызвать middleware.
Сама функция Register<ServiceName>HandlerServer выглядит примерно так:
func RegisterTestServiceHandlerServer(
ctx context.Context,
mux *runtime.ServeMux,
server TestServiceServer // я добавлю middleware после аргумента server
) error {
mux.Handle("POST", pattern_TestService_MethodOne_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/sdk.TestService/MethodOne", runtime.WithHTTPPathPattern("/sdk.TestService/MethodOne"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
// внутри функции local_request_TestService_MethodOne_0 вызывается уже сама
// бизнес логика, поэтому нам надо вызвать интерсептор до этого момента
resp, md, err := local_request_TestService_MethodOne_0(
annotatedContext,
inboundMarshaler,
server,
req,
pathParams,
)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_TestService_MethodOne_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
После прохода написанного мной .proto платина я хочу увидеть что то такое:
func RegisterTestServiceHandlerServer(
ctx context.Context,
mux *runtime.ServeMux,
server TestServiceServer,
interceptor grpc.UnaryServerInterceptor, // вот наша миддлваря
) error {
mux.Handle("POST", pattern_TestService_MethodOne_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/sdk.TestService/MethodOne", runtime.WithHTTPPathPattern("/sdk.TestService/MethodOne"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
// теперь тут новая функция, которая принимает внутрь себя интерсептор
md, resp, err := interceptor_local_request_TestService_MethodOne_0(
annotatedContext,
inboundMarshaler,
server,
interceptor,
req,
pathParams,
)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_TestService_MethodOne_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// данная функция затевалась всего лишь как обертка для
// local_request_TestService_MethodOne_0 которая внутри себя вызывает
// инетресептор
func interceptor_local_request_TestService_MethodOne_0(
annotatedContext context.Context,
inboundMarshaler runtime.Marshaler,
server UsersServiceServer,
interceptor grpc.UnaryServerInterceptor,
req *http.Request,
pathParams map[string]string) (md runtime.ServerMetadata, resp proto.Message, err error) {
type handlerResponse struct {
md runtime.ServerMetadata
resp proto.Message
}
handler := func(ctx context.Context, req any) (any, error) {
if req, ok := req.(*http.Request); ok {
resp, md, err := local_request_TestService_MethodOne_0(annotatedContext, inboundMarshaler, server, req, pathParams)
return handlerResponse{resp: resp, md: md}, err
}
return nil, fmt.Errorf("error converting req to *http.Request")
}
var handlerResponseItem any
// если интерсептор будет равен nil тогда выполняем все без него
if interceptor == nil {
handlerResponseItem, err = handler(annotatedContext, req)
} else {
handlerResponseItem, err = interceptor(annotatedContext, req, &grpc.UnaryServerInfo{Server: server, FullMethod: "/sdk.UsersService/GetRetoolUsersList"}, handler)
}
if err != nil {
return
}
data, ok := handlerResponseItem.(handlerResponse)
if !ok {
return
}
return data.md, data.resp, nil
}
После того как плагин поправит код, можно будет сделать так:
err = sdk.RegisterTestHandlerServer(ctx, g, handlers.grpcHandler, middleware)
if err != nil {
return nil, fmt.Errorf("error while initing handlers %w", err)
}
Теперь мы можем прокинуть middleware в RegisterTestHandlerServer и мы довольны)))
В следующей статье я могу могу рассказать подробнее как можно модифицировать код с помощью acl.
Код моего платина находится тут:
https://github.com/tarmalonchik/protoc-gen-interceptors