Custom protobuf options of message of type Any in Go

Issue

I have a GRPC service defined like:

message SendEventRequest {
  string producer = 1;
  google.protobuf.Any event = 2;
}

message SendEventResponse {
  string event_name = 1;
  string status = 2;
}

service EventService {
  rpc Send(SendEventRequest) returns (SendEventResponse);
}

I also have defined a custom message option:

extend google.protobuf.MessageOptions {
  // event_name is the unique name of the event sent by the clients
  string event_name = 50000;
}

What I want to achieve is have clients create custom proto messages that set the event_name option to a "constant". For instance:

message SomeCustomEvent {
  option (mypackage.event_name) = "some_custom_event";

  string data = 1;
  ...
}

That way the service can keep track of what events are being sent. When I do something like this I’m able to get the value of the option from a specific proto.Message:

_, md := descriptor.MessageDescriptorProto(SomeCustomEvent)
mOpts := md.GetOptions()
eventName := proto.GetExtension(mOpts, mypackage.E_EventName)

However, when the message is of type github.com/golang/protobuf/ptypes/any.Any the options are nil. How can I retrieve the event_name from the message? I’ve come across the protoregistry.MessageTypeResolver, which looks like it might help, but I would need to figure out a way to dynamically update the proto definitions of the events when clients integrate.

Solution

In order to obtain the options of an Any type, you need its specific protoreflect.MessageType so that you can unmarshal it into a specific message. In order to get the message type, you need a MessageTypeResolver.

Any contains a type_url field, which can be used for that purpose. In order to unmarshal the Any object into a message of an existing message type:

// GlobalTypes contains information about the proto message types
var res protoregistry.MessageTypeResolver = protoregistry.GlobalTypes
typeUrl := anyObject.GetTypeUrl()
msgType, _ := res.FindMessageByURL(typeUrl)

msg := msgType.New().Interface()
unmarshalOptions := proto.UnmarshalOptions{Resolver: res}
unmarshalOptions.Unmarshal(anyObject.GetValue(), msg)

After having the specific message, you can simply get the option you need:

msgOpts := msg.ProtoReflect().Descriptor().Options()
eventName := proto.GetExtension(msgOpts, mypackage.E_EventName)

Note that proto.GetExtension will panic if the message doesn’t extend the event_name option, and it needs to be recovered. This block can be added at the beginning of the function:

defer func() {
    if r := recover(); r != nil {
        // err is a named return parameter of the outer function
        err = fmt.Errorf("recovering from panic while extracting event_name from proto message: %s", r)
    }
}()

EDIT: Note that the application has to import the package containing the proto definitions in order for protoregistry.GlobalTypes to recognize the type. You could do something like this in your code:

var _ mypackage.SomeEvent

Answered By – Boyan Kushlev

Answer Checked By – Jay B. (GoLangFix Admin)

Leave a Reply

Your email address will not be published.