Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

status/Details: unexpected code breaks after upgrading to v1.62.0 and higher #7679

Open
povsister opened this issue Sep 29, 2024 · 21 comments · May be fixed by #7724
Open

status/Details: unexpected code breaks after upgrading to v1.62.0 and higher #7679

povsister opened this issue Sep 29, 2024 · 21 comments · May be fixed by #7724
Assignees
Labels
Area: RPC Features Includes Compression, Encoding, Attributes/Metadata, Interceptors. Type: Bug

Comments

@povsister
Copy link

What version of gRPC are you using?

v1.62.0 and higher

What version of Go are you using (go version)?

go version go1.23.1 darwin/arm64

What operating system (Linux, Windows, …) and version?

Not related

What did you do?

After upgrading to v1.62.0, we have encountered serious problem on gRPC status detail extension: Our gRPC clients can not properly retrieve the customized err detail message from gRPC status.

After some investigation, we found it is caused by PR #6919
internal/status/status.go L157-L160
https://github.com/grpc/grpc-go/pull/6919/files#diff-d78e63ce979e5c67b7fa98a36052a2529e4232343046533343d9772c0cd52549R157-R160

The problem is: status.Details always return proto.MessageV1 before, but it now implicitly returns proto.MessageV2 because of changing protoV1.UnmarshalAny to any.UnmarshalNew.
This implicit behavior actually introduces compatibility issue, though gRPC itself doesn't make any guarantees on that.

Here is an example of our code extracting customized err details.
It unexpctly breaks after upgrading to v1.62.0 and higher.

// InternalErrMessage is a protobuf-defined and generated message.
// It is generated using protobuf v1  (aka: "github.com/golang/protobuf/proto")

func GetInternalErrMessage(gst *status.Status) *InternalErrMessage {
    for _, dt := range gst.Details() {
        // details may contain proto.Message or err.
        // For now, we only deal with proto.Message.
        // PROBLEM: this does not work with grpc-go >= v1.62.0.
        // The actual type of "dt" for now is "google.golang.org/protobuf/internal/impl/*messagelfaceWrapper" which
        // only implements proto.MessageV2 interface.
        if pbMsg, ok := dt.(proto.MessageV1); ok {
            // identify msg type and do type assertion
            if protoadapt.MessageV2Of(pbMsg).ProtoReflect().Descriptor().FullName() ==
                protoadapt.MessageV2Of(&InternalErrMessage{}).ProtoReflect().Descriptor().FullName() {
                    return pbMsg.(*InternalErrMessage)
            }
        }
    }
    // some fallback logic
}

What did you expect to see?

Our Code continues to work as expected after upgrading.

What did you see instead?

Code breaks because of implicit changes of return values from gRPC.

@povsister povsister changed the title status/Details: unexpected breaking change after upgrading to v1.62.0 and higher status/Details: unexpected code breaks after upgrading to v1.62.0 and higher Sep 29, 2024
@arjan-bal arjan-bal self-assigned this Oct 1, 2024
@arjan-bal arjan-bal added the Area: RPC Features Includes Compression, Encoding, Attributes/Metadata, Interceptors. label Oct 1, 2024
@arjan-bal
Copy link
Contributor

arjan-bal commented Oct 1, 2024

I tried casting the output of status.Details() to a protoiface.MessageV1 in a unit test and it worked: https://github.com/grpc/grpc-go/compare/v1.62.x...arjan-bal:test-status-details?expand=1

@povsister can you share the generated .go file OR the protoc, protoc-gen-go versions used to generate the Go code?

@povsister
Copy link
Author

@arjan-bal
Thank you for the investigation. The pb.go file in the test you given in that link actually implements both v1 and v2 message interface. That's the reason you can't reproduce this issue.

@povsister can you share the generated .go file OR the protoc, protoc-gen-go versions used to generate the Go code?

I am not sure which version of protoc and protoc-gen-go are used for our code generation, cause there is no comments on it.
But I am sure the generated message only implements proto.MessageV1, not both v1 and v2.

Here is the snippet of the generated code.

package pb

import (
	fmt "fmt"

	proto "github.com/golang/protobuf/proto"

	math "math"

	anypb "google.golang.org/protobuf/types/known/anypb"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package

type Error struct {
	ErrCode              int32      `protobuf:"varint,1,opt,name=err_code,json=errCode,proto3" json:"err_code,omitempty"`
	ErrMessage           string     `protobuf:"bytes,2,opt,name=err_message,json=errMessage,proto3" json:"err_message,omitempty"`
	ErrDetail            *anypb.Any `protobuf:"bytes,3,opt,name=err_detail,json=errDetail,proto3" json:"err_detail,omitempty"`
	XXX_NoUnkeyedLiteral struct{}   `json:"-"`
	XXX_unrecognized     []byte     `json:"-"`
	XXX_sizecache        int32      `json:"-"`
}

func (m *Error) Reset()         { *m = Error{} }
func (m *Error) String() string { return proto.CompactTextString(m) }
func (*Error) ProtoMessage()    {}
func (*Error) Descriptor() ([]byte, []int) {
	return fileDescriptor_ecode_9897508b96e13b12, []int{0}
}
func (m *Error) XXX_Unmarshal(b []byte) error {
	return xxx_messageInfo_Error.Unmarshal(m, b)
}
func (m *Error) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
	return xxx_messageInfo_Error.Marshal(b, m, deterministic)
}
func (dst *Error) XXX_Merge(src proto.Message) {
	xxx_messageInfo_Error.Merge(dst, src)
}
func (m *Error) XXX_Size() int {
	return xxx_messageInfo_Error.Size(m)
}
func (m *Error) XXX_DiscardUnknown() {
	xxx_messageInfo_Error.DiscardUnknown(m)
}

var xxx_messageInfo_Error proto.InternalMessageInfo

func (m *Error) GetErrCode() int32 {
	if m != nil {
		return m.ErrCode
	}
	return 0
}

func (m *Error) GetErrMessage() string {
	if m != nil {
		return m.ErrMessage
	}
	return ""
}

func (m *Error) GetErrDetail() *anypb.Any {
	if m != nil {
		return m.ErrDetail
	}
	return nil
}

func init() {
	proto.RegisterType((*Error)(nil), "err.Error")
}

func init() {
	proto.RegisterFile("status/internal/pb/ecode.proto", fileDescriptor_ecode_9897508b96e13b12)
}

var fileDescriptor_ecode_9897508b96e13b12 = []byte{
	// 206 bytes of a gzipped FileDescriptorProto
	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x8f, 0x3f, 0x4f, 0xc3, 0x30,
	0x10, 0xc5, 0x15, 0xaa, 0x02, 0x75, 0x37, 0x8b, 0x21, 0x65, 0x21, 0x62, 0xca, 0x00, 0x3e, 0x89,
	0x7e, 0x02, 0xfe, 0x8d, 0x2c, 0x19, 0x59, 0x2a, 0x3b, 0x3e, 0xac, 0x48, 0x8e, 0x2f, 0x7a, 0x31,
	0x82, 0x7c, 0x7b, 0x94, 0x44, 0xcc, 0x1d, 0xef, 0xfd, 0xee, 0xee, 0xa7, 0xa7, 0x4c, 0xec, 0x1c,
	0x2c, 0x26, 0x4a, 0x9c, 0x09, 0x43, 0x4b, 0x3f, 0x16, 0x9e, 0x13, 0x75, 0x29, 0x33, 0x92, 0x8d,
	0x34, 0x38, 0xe2, 0x56, 0x3c, 0x9b, 0x01, 0x92, 0x45, 0x6f, 0x18, 0xb8, 0x3d, 0x04, 0x91, 0x10,
	0x99, 0x96, 0xc8, 0x7d, 0x7f, 0x91, 0x4d, 0xd3, 0xca, 0xef, 0x7f, 0xd5, 0xf6, 0x1d, 0x10, 0xe8,
	0x83, 0xba, 0x66, 0xe0, 0x34, 0x9f, 0x96, 0x45, 0x55, 0xd4, 0xdb, 0xe6, 0x8a, 0x81, 0x57, 0xf1,
	0xac, 0xef, 0xd4, 0x7e, 0x46, 0x3d, 0x8f, 0xa3, 0x0d, 0x5c, 0x5e, 0x54, 0x45, 0xbd, 0x6b, 0x14,
	0x03, 0x1f, 0x6b, 0xa2, 0x8f, 0x6a, 0x9e, 0x4e, 0x9e, 0xb3, 0xed, 0x62, 0xb9, 0xa9, 0x8a, 0x7a,
	0xff, 0x74, 0x63, 0x56, 0xa9, 0xf9, 0x97, 0x9a, 0xe7, 0x34, 0x35, 0x3b, 0x06, 0xde, 0x96, 0xb5,
	0x17, 0xf3, 0xf9, 0x10, 0xe4, 0xb1, 0x95, 0xbe, 0x97, 0x44, 0xe7, 0x5b, 0xb9, 0xcb, 0xe5, 0xd1,
	0xf1, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x4e, 0xef, 0xdc, 0x8d, 0x02, 0x01, 0x00, 0x00,
}

@arjan-bal
Copy link
Contributor

Thanks for the details. I was able to repro the issue after generating a pb.go file with protoc-gen-go v1.3.5.
https://github.com/grpc/grpc-go/compare/v1.62.x...arjan-bal:test-status-details?expand=1

@povsister
Copy link
Author

Great! Please keep me updated if you are going to fix that.
I need adapt our code to future designs of gRPC.

@arjan-bal
Copy link
Contributor

arjan-bal commented Oct 1, 2024

@povsister I discussed the issue with other maintainers. The change (#6919) seems to be backward incompatible when using old generated protobuf code. Since we've not gotten other reports about this issue since 1.62.0 was released 6 months ago, we don't want to make a change now.

You should be able to unblock yourself by either regenerating the pb.go files or casting the result of status.Details() into the V2 message format. I'm closing this issue, please feel free to re-open it in case you have more questions.

@povsister
Copy link
Author

povsister commented Oct 2, 2024

The change seems to be backward compatible when using old generated protobuf code.

Not exactly, at least you have reproduced this issue with protoc-gen-go v1.3.5, which is released 5 years ago or former.
It proves that legacy code or deps written with protoV1 only may encounter such corruption without any warning, too.

Since we've not gotten other reports about this issue since 1.62.0 was released 6 months ago, we don't want to make a change now.

That's ok to me. But, as I have mentioned in the thread, gRPC doesn't make any guarantees on return types of status.Detail().
It simply says:

// Details returns a slice of details messages attached to the status.
// If a detail cannot be decoded, the error is returned in place of the detail.

which is quite confusing:

  • What is details messages ? What type of message will I get ?
  • How can I cast it into concrete type of generated message? OR I should use "reflect view" of message?

Those questions are still unclear, the only way we can figure it out is to read the code of gRPC. But it doesn't help much on keeping long-term compatibility, that's the reason I am still here.

Let me give you a detail example of what I am concerned.
We have already known, once anypb unmarshal succecced, we can call Interface() to get underlying typed message and safely cast into concrete type.
After reading the code of status.Details, we assume that we can treat the return values as slice of "anypb unmarshaled with Interface() already called".

So, 2 ways beneath should work.

  • Cast into MessageV1 if it is generated in v1 format.
  • Cast into correct concreat type of message.

They did work before v1.62.0, But both fails after upgraing to grpc-go >= v1.62.0.

I have several suggestion:

  • Always return concrete type of messages in status.Detail
  • Clarify the return types of status.Details in doc
  • Add best practice in status doc.

@arjan-bal arjan-bal reopened this Oct 3, 2024
@arjan-bal
Copy link
Contributor

arjan-bal commented Oct 3, 2024

The change seems to be backward compatible when using old generated protobuf code.

I meant to say the change is backaward "incompatible". Sorry about the confusion.

I was able to get the failing test in the repro to pass by wrapping the messages using protoadapt.MessageV1Of() status.Details() before returning. This should ensure the messages always implement the proto V1 interface. This would then allow us to update the doc comment of status.Details to mention that the returned message is in the v1 format.

Let me get opinions from the team and get back.

@povsister
Copy link
Author

Okay, I was feeling weird but didn't aware that was a typo.

This should ensure the messages always implement the proto V1 interface. This would then allow us to update the doc comment of status.Details to mention that the returned message is in the v1 format.

Since there is no more report about this issue, I prefer make some enhancement instead of keeping 100% compatibility.
Yes, by wrapping them all with protoadapt.MessageV1Of() makes 100% same return values as before, but in practice we often cast it into concrete messages cause we need to access fields inside.

Thus, I think concrete messages of return values make more sense in this situation.
And we can make guarantees saying users can always directly cast it into generated messages or errors.

@arjan-bal
Copy link
Contributor

Wouldn't changing the return type require a new API because the current status.Details() returns a slice of "either an error OR proto message"?

@povsister
Copy link
Author

Literally speaking, I don't think this is a API change cause the return type of status.Details() is a slice of any, which accepts all values in Go's type system.

The key is "what type of values gRPC guarantees to return?"

There is no guarantees before, so we inferred it by reading code of gRPC. But it has been implicitly/unintentionally changed during legacy deps cleanup for more than 6 months and nobody except us noticed. It proves this is not very risky, so I think introducing new a API is not necessary. It's more important to make things right and elegant.

@arjan-bal
Copy link
Contributor

arjan-bal commented Oct 4, 2024

But it has been implicitly/unintentionally changed during legacy deps cleanup for more than 6 months and nobody except us noticed. It proves this is not very risky

There are users of the status.Details API, I believe no one faced a similar issue because it requires using old generated protobuf code. protoc-gen-go v1.4 was released in 2020 while the #6919 was merged in Jan 2024. So people who re-generated their protobuf code in the 3-4 year window wouldn't be effected.

The doc comment is also a contract, even though it isn't enforced by the compiler. If we were to ensure that the status.Details method returns an error or a proto message with stricter type, we would need to return something that implements both interfaces, Error and protoiface.MessageV1.

type Ret interface {
    error
    protoiface.MessageV1
}

However, implementing both interface is not the same as implementing either of the interface. So the code that does a type switch on the returned value will never execute the second case statement after the change:

    switch v := a.(type) {
    case protoiface.MessageV1:
        // Always gets executed now.
    case error:
        // Dead code.
    }

Based on this, I'm planning to keep the return types the same to avoid breaking users. I'll make the change to ensure that the returned value implements protoimpl.MessageV1 using protoadapt.MessageV1Of.

@povsister
Copy link
Author

povsister commented Oct 4, 2024

Sorry I just realized I might not make myself clear.

I am not suggesting narrow return type of status.Details() to something that enforced by compiler. I meant gRPC should make more practical and precise contract on actual return values through comments/doc.
Then, we are discussing what else can be done except making more clear comments and keeping 100% same return values as before.

As you have written above, using protoadapt.MessageV1Of on returned proto.Message is one approach to solve this issue, but I don't think this is the right way.

Actually, this issue is caused by implicitly wrapping old-generated v1 messages with messagelfaceWrapper, the purpose is to make sure unmarshaled message returned by anypb.UnmarshalNew always implement MessageV2, which sounds quite reasonable.
But wrapping it up cause type-switch fails both on protoiface.MessageV1 and old-generated Go types.

What's worse, the approach you suggest may cause future problems if gRPC decides to remove MessageV1 implementation on newly generated code.
You will see a "wrapper" on generated v2 messages and no chance to remove because you have made promise in this issue!
Also, users will unable to directly cast it into generated Go struct unless protoadapt.MessageV2Of called.
It's quite ridiculous on user experience: our code stop working after a re-generation of pb files.

So I belive the root problem is: We might don't need to wrap it, just returning the vanilla-generated Go structs registered in protobuf registry is more viable.
This is the second approach I prefer. I'll describe it with code below.

  1. Changing comments on status.Details()
  2. Changing unmarshal procedures inside status.Details(). See code below
// Details returns a slice of proto.Message(either v1 or v2) attached to the status.
// If a detail cannot be decoded through anypb.Unmarshal, the error is returned in place of the detail.
// It is guaranted that proto.Message is in its vanilla type registered in protobuf registry.
// So users can always try casting it into generated vanilla Go structs, 
// or you can cast it into MessageV1/V2 interface which depends on the generated code.
func (s *Status) Details() []any {
  // 1. Get a instance of vanilla Go type registered in protobuf registry.
  // 2. Using `anypb.UnmarshalTo` to set data into Go structs.
  // 3. Return instance from 1 or error from 2, without wrapping. 
}

I am not good at English, if you have anything unclear, please let me know.

@povsister
Copy link
Author

povsister commented Oct 4, 2024

In addition, I just realized that: even before v1.62.0, status.Details() always return proto.Message represented by generated Go structs.
Sorry for the misleading report.

https://github.com/grpc/grpc-go/pull/6919/files#diff-d78e63ce979e5c67b7fa98a36052a2529e4232343046533343d9772c0cd52549R157-R160
In the code before v1.62.0, ptypes.UnmarshalAny uses Empty to get a zero Message from registry, with proto.MessageV1() called.

As for now, generated code from latest protoc-gen-go still implements MessageV1, which means, even with proto.MessageV1() called, a Empty call will always return vanilla-generated Go structs.(they might be v1 or v1+v2).

So literally, v1.62.0 indeed introduced a bug while unmarshaling anypb from gRPC status.
And there is no compatibility risk if we fix it.

The only problem is, this bug has been released for more than 6 months, and someone may rely on this "bug feature" to work properly.

// Empty returns a new message of the type specified in an anypb.Any message.
// It returns protoregistry.NotFound if the corresponding message type could not
// be resolved in the global registry.
//
// Deprecated: Use protoregistry.GlobalTypes.FindMessageByName instead
// to resolve the message name and create a new instance of it.
func Empty(any *anypb.Any) (proto.Message, error) {
	name, err := anyMessageName(any)
	if err != nil {
		return nil, err
	}
	mt, err := protoregistry.GlobalTypes.FindMessageByName(name)
	if err != nil {
		return nil, err
	}
	return proto.MessageV1(mt.New().Interface()), nil
}

@arjan-bal
Copy link
Contributor

@povsister to summarize, before 1.62, status.Details() used ptypes.UnmarshalAny which called proto.MessageV1(mt.New().Interface()) which in turn called protoimpl.X.ProtoMessageV1Of(m) so the returned type always implemented protoadapt.MessageV1.

The reported issue is caused because anypb.UnmarshallNew() doesn't wrap the proto in a MessageV1. Without wrapping, the message type returned from the proto registry doesn't implement the V1 interface when code is generated using protoc-gen-go < 1.4.

func getMessageInstance(anyMsg *anypb.Any) (proto.Message, error) {
	// Lookup the message type in the global registry
	msgType, err := protoregistry.GlobalTypes.FindMessageByURL(anyMsg.GetTypeUrl())
	if err != nil {
		return nil, fmt.Errorf("could not find message type: %v", err)
	}

	// Create a new instance of the message type
	msg := msgType.New().Interface()
	if _, ok := msg.(protoadapt.MessageV1); !ok {
		fmt.Println("Message doesn't implement V1 interface", msg)
	}

	return msg, nil
}

If this is true, then the second approach suggested by you in #7679 (comment) doesn't work, right?

@arjan-bal
Copy link
Contributor

Can you give an example code snippet that would break if we make the change to wrap the status protos inprotoimpl.MessageV1 as I suggested in #7679 (comment)?

@povsister
Copy link
Author

to summarize, before 1.62, status.Details() used ptypes.UnmarshalAny which called proto.MessageV1(mt.New().Interface()) which in turn called protoimpl.X.ProtoMessageV1Of(m) so the returned type always implemented protoadapt.MessageV1.

Yes, the returned type always implements protoadapt.MessageV1. But this is not the key point.

The key point is: even before 1.62, status.Details() always return vanilla-generated Go structs, because protoc-gen-go always generates pb.go with MessageV1 implementation. An additional call to proto.MessageV1() actually did nothing but confirmed return values are in their vanilla form before 1.62.

Adding proto.MessageV1() do solve this issue, but, adding proto.MessageV1() and making contract on returns values will cause problem if generated code removes MessageV1 implementation oneday in the future. It will add unwanted wrappers on v2 only messages, just to satisfy what we have promised.



The reported issue is caused because anypb.UnmarshallNew() doesn't wrap the proto in a MessageV1. Without wrapping, the message type returned from the proto registry doesn't implement the V1 interface when code is generated using protoc-gen-go < 1.4.

No, this does not work as you thought.
anypb.UnmarshallNew() uses msgType.New().Interface() to allocate a instance of given msgType, and it is guaranteed that Interface() returns the underlying ProtoMessage interface, which equals to MessageV2.

// Interface unwraps the message reflection interface and
// returns the underlying ProtoMessage interface.
Interface() ProtoMessage

So, there is nothing to do with anypb.UnmarshallNew(), It works as expected: wrapping v1 only messages so they can have additional "v2 capability".

The problem is: wrapped message is directly returned by status.Details() after 1.62, which is not in its vanilla form.
And this is what I always insist: return vanilla form of messages, avoiding wrapping.



If this is true, then the second approach suggested by you in #7679 (comment) doesn't work, right?

Yes, the snippet given by your comments will fail on this issue.
It is just pseudo code describing what I am thinking. We need figure out a way to get a vanilla and unwrapped instance by typeUrl.



Can you give an example code snippet that would break if we make the change to wrap the status protos in protoimpl.MessageV1 as I suggested in #7679 (comment)?

Sure, as I have said, if we have v2 only messages oneday in the future, wrapping return values with protoimpl.MessageV1 will introduce similar issue.

// generated message which only implements MessageV2 interface
type V2OnlyMessage struct {
	...
}

func (m *V2OnlyMessage) ProtoReflect() protoreflect.Message {
	...
}

func getCustomizedMsgFromGST(gst *status.Status) (*V2OnlyMessage, error) {
  for _, st := range gst.Details() {
    var v2Msg protoV2.Message
    switch pbMsg := st.(type) {
    case protoV2.Message:
      v2Msg = pbMsg
    case proto.Message:
      v2Msg = protoadapt.MessageV2Of(pbMsg)
    default:
      return nil, fmt.Errorf("not a proto message: %T", st)
    }
    if v2Msg.ProtoReflect().Descriptor().FullName() ==
      (*V2OnlyMessage)(nil).ProtoReflect().Descriptor().FullName() {
      // This works before 1.62, and also works for now.
      // But it will fail if protoadapt.MessageV1Of() called inside status.Details().
      // Because return values will be wrapped, just like what we are discussing.
      if retMsg, ok := v2Msg.(*V2OnlyMessage); ok {
        return retMsg, nil
      } else {
        // How can we safely cast it into vanilla form of Go structs ??
      }
    }
  }
}

In a nutshell, adding proto.MessageV1() is one solution for now, but it will break if v1 is completely removed from pb.go.
We'd better always return vanilla form of message as I have written in previous comments:

// Details returns a slice of proto.Message(either v1 or v2) attached to the status.
// If a detail cannot be decoded through anypb.Unmarshal, the error is returned in place of the detail.
// It is guaranted that proto.Message is in its vanilla type registered in protobuf registry.
// So users can always try casting it into generated vanilla Go structs, 
// or you can cast it into MessageV1/V2 interface which depends on the generated code.

@arjan-bal
Copy link
Contributor

arjan-bal commented Oct 8, 2024

In a nutshell, adding proto.MessageV1() is one solution for now, but it will break if v1 is completely removed from pb.go.

This is a problem that gRPC Go will need to be solve when the V1 API is removed anyways, since the status.WithDetails() API used to store details accepts protoadapt.MessageV1s at the moment. This also means that all the messages stored MUST implement the V1 API.

// WithDetails returns a new status with the provided details messages appended to the status.
// If any errors are encountered, it returns nil and the first error encountered.
func (s *Status) WithDetails(details ...protoadapt.MessageV1) (*Status, error) {

@povsister
Copy link
Author

povsister commented Oct 8, 2024

Yeah, I agree that this is a problem that gRPC Go will need to be solve in the future.

But, code will suddenly stop working if user re-generates their pb.go.
This is not like: There is a release note of gRPC-go saying we are going to break something. Everything happens behind scene, users will get confused because they even don't have grpc-go updated.
And, what I have suggested, is actually compatible with everything before 1.62.

since the status.WithDetails() API used to store details accepts protoadapt.MessageV1s at the moment. This also means that all the messages stored MUST implement the V1 API.

I don't think they are equivalent. status.WithDetails() require protoadapt.MessageV1 because it is required by Go's type system, and there is obviously no MessageV2 at the time it was written.
This does not means they should be returned in V1 format in status.Details. Since there is no contract before, the normal way is to keep exactly same values as before: always returning vanilla form of message.

Implicite wrappers have already caused one problem here, I wish not see same thing happens again in future.

@povsister
Copy link
Author

Let me make it more clear and practical:

  1. Adding proto.MessageV1() is acceptable for now.
  2. Comments should be made on status.Details saying it will always return vanilla form of proto messages. Do NOT guarantee that it will return proto messages in v1 format.
    Because "it will return proto messages in v1 format" is actually useless and out of usage pattern.
  3. Leaving a TODO on status.Details, reminding other maintainers of updating it once internal API changed (if any).

@arjan-bal
Copy link
Contributor

To clarify, is the following understanding correct?

msgType.New().Interface() always returns MessageV2. In case the underlying message is a wrapped MessageV1 which doesn't implement MessageV2 itself, calling protoadapt.MessageV1Of() unwraps and returns the original MessageV1.

@povsister
Copy link
Author

msgType.New().Interface() always returns MessageV2. In case the underlying message is a wrapped MessageV1 which doesn't implement MessageV2 itself, calling protoadapt.MessageV1Of() unwraps and returns the original MessageV1.

Totally correct

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: RPC Features Includes Compression, Encoding, Attributes/Metadata, Interceptors. Type: Bug
Projects
None yet
2 participants