Repository: grpc/grpc-go

Issue:
status/Details: unexpected code breaks after upgrading to v1.62.0 and higher
### 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.
```go

// 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.


Hints from issue comments:
@arjan-bal: 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?@Einsfier: @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.

```go
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: 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@Einsfier: Great! Please keep me updated if you are going to fix that.
I need adapt our code to future designs of gRPC.@arjan-bal: @povsister I discussed the issue with other maintainers. The change (https://github.com/grpc/grpc-go/pull/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.@Einsfier: > 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: > 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](https://github.com/grpc/grpc-go/commit/5e37f50eb2510ad88664f146e0e070155a437139) 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.@Einsfier: 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: 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"?@Einsfier: 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: >  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 https://github.com/grpc/grpc-go/pull/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`. 
```go
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:
```go
    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`.@Einsfier: 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

```go
// 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.@Einsfier: 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.

```go
// 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: @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`.
```go
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 https://github.com/grpc/grpc-go/issues/7679#issuecomment-2393138580 doesn't work, right?@arjan-bal: 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 https://github.com/grpc/grpc-go/issues/7679#issuecomment-2392929547?@Einsfier: > 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.

<br/>
<br/>

> 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`.

```go
// 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.

<br/>
<br/>

> If this is true, then the second approach suggested by you in https://github.com/grpc/grpc-go/issues/7679#issuecomment-2393138580 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`.

<br/>
<br/>

> 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 https://github.com/grpc/grpc-go/issues/7679#issuecomment-2392929547?

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.


```go
// 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: > 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.MessageV1`s at the moment. This also means that all the messages stored MUST implement the V1 API.
https://github.com/grpc/grpc-go/blob/bdd444d1780717525454d041b0fc657d53da31f1/internal/status/status.go#L132-L134@Einsfier: 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.@Einsfier: 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: 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`.@Einsfier: > `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@arjan-bal: The fix is expected be released as part of grpc-go v1.69 since we've already cut the branch for 1.68.@nghialv: @arjan-bal Hi, we are facing the same issue in our organization. May I ask when this will be released?@arjan-bal: @nghialv the fix was [cherry-picked into the version 1.68 release branch](https://github.com/grpc/grpc-go/pull/7793). grpc-go 1.68 should be released this week. @nghialv: @arjan-bal Great to hear! Thank you so much for your work.

Solution patch:
diff --git a/credentials/tls.go b/credentials/tls.go
index 41143585..e163a473 100644
--- a/credentials/tls.go
+++ b/credentials/tls.go
@@ -200,25 +200,40 @@ var tls12ForbiddenCipherSuites = map[uint16]struct{}{
 
 // NewTLS uses c to construct a TransportCredentials based on TLS.
 func NewTLS(c *tls.Config) TransportCredentials {
-	tc := &tlsCreds{credinternal.CloneTLSConfig(c)}
-	tc.config.NextProtos = credinternal.AppendH2ToNextProtos(tc.config.NextProtos)
+	config := applyDefaults(c)
+	if config.GetConfigForClient != nil {
+		oldFn := config.GetConfigForClient
+		config.GetConfigForClient = func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
+			cfgForClient, err := oldFn(hello)
+			if err != nil || cfgForClient == nil {
+				return cfgForClient, err
+			}
+			return applyDefaults(cfgForClient), nil
+		}
+	}
+	return &tlsCreds{config: config}
+}
+
+func applyDefaults(c *tls.Config) *tls.Config {
+	config := credinternal.CloneTLSConfig(c)
+	config.NextProtos = credinternal.AppendH2ToNextProtos(config.NextProtos)
 	// If the user did not configure a MinVersion and did not configure a
 	// MaxVersion < 1.2, use MinVersion=1.2, which is required by
 	// https://datatracker.ietf.org/doc/html/rfc7540#section-9.2
-	if tc.config.MinVersion == 0 && (tc.config.MaxVersion == 0 || tc.config.MaxVersion >= tls.VersionTLS12) {
-		tc.config.MinVersion = tls.VersionTLS12
+	if config.MinVersion == 0 && (config.MaxVersion == 0 || config.MaxVersion >= tls.VersionTLS12) {
+		config.MinVersion = tls.VersionTLS12
 	}
 	// If the user did not configure CipherSuites, use all "secure" cipher
 	// suites reported by the TLS package, but remove some explicitly forbidden
 	// by https://datatracker.ietf.org/doc/html/rfc7540#appendix-A
-	if tc.config.CipherSuites == nil {
+	if config.CipherSuites == nil {
 		for _, cs := range tls.CipherSuites() {
 			if _, ok := tls12ForbiddenCipherSuites[cs.ID]; !ok {
-				tc.config.CipherSuites = append(tc.config.CipherSuites, cs.ID)
+				config.CipherSuites = append(config.CipherSuites, cs.ID)
 			}
 		}
 	}
-	return tc
+	return config
 }
 
 // NewClientTLSFromCert constructs TLS credentials from the provided root
diff --git a/go.mod b/go.mod
index d87907da..80f75508 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
 	github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78
 	github.com/envoyproxy/go-control-plane v0.13.1
 	github.com/golang/glog v1.2.2
+	github.com/golang/protobuf v1.5.4
 	github.com/google/go-cmp v0.6.0
 	github.com/google/uuid v1.6.0
 	golang.org/x/net v0.30.0
diff --git a/go.sum b/go.sum
index c6241621..8ed71623 100644
--- a/go.sum
+++ b/go.sum
@@ -16,6 +16,8 @@ github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6
 github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
 github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
 github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
diff --git a/internal/status/status.go b/internal/status/status.go
index 75792538..1186f1e9 100644
--- a/internal/status/status.go
+++ b/internal/status/status.go
@@ -149,6 +149,8 @@ func (s *Status) WithDetails(details ...protoadapt.MessageV1) (*Status, error) {
 
 // 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.
+// If the detail can be decoded, the proto message returned is of the same
+// type that was given to WithDetails().
 func (s *Status) Details() []any {
 	if s == nil || s.s == nil {
 		return nil
@@ -160,7 +162,38 @@ func (s *Status) Details() []any {
 			details = append(details, err)
 			continue
 		}
-		details = append(details, detail)
+		// The call to MessageV1Of is required to unwrap the proto message if
+		// it implemented only the MessageV1 API. The proto message would have
+		// been wrapped in a V2 wrapper in Status.WithDetails. V2 messages are
+		// added to a global registry used by any.UnmarshalNew().
+		// MessageV1Of has the following behaviour:
+		// 1. If the given message is a wrapped MessageV1, it returns the
+		//   unwrapped value.
+		// 2. If the given message already implements MessageV1, it returns it
+		//   as is.
+		// 3. Else, it wraps the MessageV2 in a MessageV1 wrapper.
+		//
+		// Since the Status.WithDetails() API only accepts MessageV1, calling
+		// MessageV1Of ensures we return the same type that was given to
+		// WithDetails:
+		// * If the give type implemented only MessageV1, the unwrapping from
+		//   point 1 above will restore the type.
+		// * If the given type implemented both MessageV1 and MessageV2, point 2
+		//   above will ensure no wrapping is performed.
+		// * If the given type implemented only MessageV2 and was wrapped using
+		//   MessageV1Of before passing to WithDetails(), it would be unwrapped
+		//   in WithDetails by calling MessageV2Of(). Point 3 above will ensure
+		//   that the type is wrapped in a MessageV1 wrapper again before
+		//   returning. Note that protoc-gen-go doesn't generate code which
+		//   implements ONLY MessageV2 at the time of writing.
+		//
+		// NOTE: Status details can also be added using the FromProto method.
+		// This could theoretically allow passing a Detail message that only
+		// implements the V2 API. In such a case the message will be wrapped in
+		// a MessageV1 wrapper when fetched using Details().
+		// Since protoc-gen-go generates only code that implements both V1 and
+		// V2 APIs for backward compatibility, this is not a concern.
+		details = append(details, protoadapt.MessageV1Of(detail))
 	}
 	return details
 }
diff --git a/interop/xds/go.sum b/interop/xds/go.sum
index ef695b39..16f77351 100644
--- a/interop/xds/go.sum
+++ b/interop/xds/go.sum
@@ -23,6 +23,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
diff --git a/scripts/regenerate.sh b/scripts/regenerate.sh
index 282774b1..55fa279d 100755
--- a/scripts/regenerate.sh
+++ b/scripts/regenerate.sh
@@ -26,9 +26,9 @@ export PATH="${GOBIN}:${PATH}"
 mkdir -p "${GOBIN}"
 
 echo "removing existing generated files..."
-# grpc_testing_not_regenerate/*.pb.go is not re-generated,
-# see grpc_testing_not_regenerate/README.md for details.
-find . -name '*.pb.go' | grep -v 'grpc_testing_not_regenerate' | xargs rm -f || true
+# grpc_testing_not_regenerated/*.pb.go is not re-generated,
+# see grpc_testing_not_regenerated/README.md for details.
+find . -name '*.pb.go' | grep -v 'grpc_testing_not_regenerated' | xargs rm -f || true
 
 echo "Executing: go install google.golang.org/protobuf/cmd/protoc-gen-go..."
 (cd test/tools && go install google.golang.org/protobuf/cmd/protoc-gen-go)
@@ -124,8 +124,8 @@ done
 mkdir -p "${WORKDIR}/out/google.golang.org/grpc/internal/proto/grpc_lookup_v1"
 mv "${WORKDIR}"/out/google.golang.org/grpc/lookup/grpc_lookup_v1/* "${WORKDIR}/out/google.golang.org/grpc/internal/proto/grpc_lookup_v1"
 
-# grpc_testing_not_regenerate/*.pb.go are not re-generated,
-# see grpc_testing_not_regenerate/README.md for details.
-rm "${WORKDIR}"/out/google.golang.org/grpc/reflection/test/grpc_testing_not_regenerate/*.pb.go
+# grpc_testing_not_regenerated/*.pb.go are not re-generated,
+# see grpc_testing_not_regenerated/README.md for details.
+rm "${WORKDIR}"/out/google.golang.org/grpc/testdata/grpc_testing_not_regenerated/*.pb.go
 
 cp -R "${WORKDIR}"/out/google.golang.org/grpc/* .
diff --git a/scripts/vet.sh b/scripts/vet.sh
index 1d9991a5..0d2b8b8f 100755
--- a/scripts/vet.sh
+++ b/scripts/vet.sh
@@ -59,13 +59,13 @@ git grep 'func [A-Z]' -- "*_test.go" | not grep -v 'func Test\|Benchmark\|Exampl
 git grep -l 'time.After(' -- "*.go" | not grep -v '_test.go\|test_utils\|testutils'
 
 # - Do not use "interface{}"; use "any" instead.
-git grep -l 'interface{}' -- "*.go" 2>&1 | not grep -v '\.pb\.go\|protoc-gen-go-grpc\|grpc_testing_not_regenerate'
+git grep -l 'interface{}' -- "*.go" 2>&1 | not grep -v '\.pb\.go\|protoc-gen-go-grpc\|grpc_testing_not_regenerated'
 
 # - Do not call grpclog directly. Use grpclog.Component instead.
 git grep -l -e 'grpclog.I' --or -e 'grpclog.W' --or -e 'grpclog.E' --or -e 'grpclog.F' --or -e 'grpclog.V' -- "*.go" | not grep -v '^grpclog/component.go\|^internal/grpctest/tlogger_test.go\|^internal/grpclog/prefix_logger.go'
 
 # - Ensure that the deprecated protobuf dependency is not used.
-not git grep "\"github.com/golang/protobuf/*" -- "*.go" ':(exclude)reflection/test/grpc_testing_not_regenerate/*'
+not git grep "\"github.com/golang/protobuf/*" -- "*.go" ':(exclude)testdata/grpc_testing_not_regenerated/*'
 
 # - Ensure all usages of grpc_testing package are renamed when importing.
 not git grep "\(import \|^\s*\)\"google.golang.org/grpc/interop/grpc_testing" -- "*.go"
@@ -112,7 +112,7 @@ for MOD_FILE in $(find . -name 'go.mod'); do
   noret_grep -v "(ST1000)" "${SC_OUT}" | noret_grep -v "(SA1019)" | noret_grep -v "(ST1003)" | noret_grep -v "(ST1019)\|\(other import of\)" | not grep -v "(SA4000)"
 
   # Exclude underscore checks for generated code.
-  noret_grep "(ST1003)" "${SC_OUT}" | not grep -v '\(.pb.go:\)\|\(code_string_test.go:\)\|\(grpc_testing_not_regenerate\)'
+  noret_grep "(ST1003)" "${SC_OUT}" | not grep -v '\(.pb.go:\)\|\(code_string_test.go:\)\|\(grpc_testing_not_regenerated\)'
 
   # Error for duplicate imports not including grpc protos.
   noret_grep "(ST1019)\|\(other import of\)" "${SC_OUT}" | not grep -Fv 'XXXXX PleaseIgnoreUnused
@@ -149,7 +149,7 @@ XXXXX PleaseIgnoreUnused'
 XXXXX Protobuf related deprecation errors:
 "github.com/golang/protobuf
 .pb.go:
-grpc_testing_not_regenerate
+grpc_testing_not_regenerated
 : ptypes.
 proto.RegisterType
 XXXXX gRPC internal usage deprecation errors:
@@ -191,7 +191,7 @@ done
 # Error for violation of enabled lint rules in config excluding generated code.
 revive \
   -set_exit_status=1 \
-  -exclude "reflection/test/grpc_testing_not_regenerate/" \
+  -exclude "testdata/grpc_testing_not_regenerated/" \
   -exclude "**/*.pb.go" \
   -formatter plain \
   -config "$(dirname "$0")/revive.toml" \
diff --git a/security/advancedtls/examples/go.sum b/security/advancedtls/examples/go.sum
index d4393272..9102af78 100644
--- a/security/advancedtls/examples/go.sum
+++ b/security/advancedtls/examples/go.sum
@@ -1,3 +1,5 @@
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
diff --git a/security/advancedtls/go.sum b/security/advancedtls/go.sum
index d4393272..9102af78 100644
--- a/security/advancedtls/go.sum
+++ b/security/advancedtls/go.sum
@@ -1,3 +1,5 @@
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
diff --git a/stats/opencensus/go.sum b/stats/opencensus/go.sum
index d3820066..f545f284 100644
--- a/stats/opencensus/go.sum
+++ b/stats/opencensus/go.sum
@@ -859,6 +859,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
diff --git a/stats/opentelemetry/go.sum b/stats/opentelemetry/go.sum
index 14411e4f..b2d047c8 100644
--- a/stats/opentelemetry/go.sum
+++ b/stats/opentelemetry/go.sum
@@ -21,6 +21,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=



Test patch:
diff --git a/credentials/tls_ext_test.go b/credentials/tls_ext_test.go
index c817777b..22881a6f 100644
--- a/credentials/tls_ext_test.go
+++ b/credentials/tls_ext_test.go
@@ -79,43 +79,86 @@ func (s) TestTLS_MinVersion12(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
 	defer cancel()
 
-	// Create server creds without a minimum version.
-	serverCreds := credentials.NewTLS(&tls.Config{
-		// MinVersion should be set to 1.2 by gRPC by default.
-		Certificates: []tls.Certificate{serverCert},
-	})
-	ss := stubserver.StubServer{
-		EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
-			return &testpb.Empty{}, nil
+	testCases := []struct {
+		name      string
+		serverTLS func() *tls.Config
+	}{
+		{
+			name: "base_case",
+			serverTLS: func() *tls.Config {
+				return &tls.Config{
+					// MinVersion should be set to 1.2 by gRPC by default.
+					Certificates: []tls.Certificate{serverCert},
+				}
+			},
+		},
+		{
+			name: "fallback_to_base",
+			serverTLS: func() *tls.Config {
+				config := &tls.Config{
+					// MinVersion should be set to 1.2 by gRPC by default.
+					Certificates: []tls.Certificate{serverCert},
+				}
+				config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
+					return nil, nil
+				}
+				return config
+			},
+		},
+		{
+			name: "dynamic_using_get_config_for_client",
+			serverTLS: func() *tls.Config {
+				return &tls.Config{
+					GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
+						return &tls.Config{
+							// MinVersion should be set to 1.2 by gRPC by default.
+							Certificates: []tls.Certificate{serverCert},
+						}, nil
+					},
+				}
+			},
 		},
 	}
 
-	// Create client creds that supports V1.0-V1.1.
-	clientCreds := credentials.NewTLS(&tls.Config{
-		ServerName: serverName,
-		RootCAs:    certPool,
-		MinVersion: tls.VersionTLS10,
-		MaxVersion: tls.VersionTLS11,
-	})
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Create server creds without a minimum version.
+			serverCreds := credentials.NewTLS(tc.serverTLS())
+			ss := stubserver.StubServer{
+				EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
+					return &testpb.Empty{}, nil
+				},
+			}
 
-	// Start server and client separately, because Start() blocks on a
-	// successful connection, which we will not get.
-	if err := ss.StartServer(grpc.Creds(serverCreds)); err != nil {
-		t.Fatalf("Error starting server: %v", err)
-	}
-	defer ss.Stop()
+			// Create client creds that supports V1.0-V1.1.
+			clientCreds := credentials.NewTLS(&tls.Config{
+				ServerName: serverName,
+				RootCAs:    certPool,
+				MinVersion: tls.VersionTLS10,
+				MaxVersion: tls.VersionTLS11,
+			})
 
-	cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(clientCreds))
-	if err != nil {
-		t.Fatalf("grpc.NewClient error: %v", err)
-	}
-	defer cc.Close()
+			// Start server and client separately, because Start() blocks on a
+			// successful connection, which we will not get.
+			if err := ss.StartServer(grpc.Creds(serverCreds)); err != nil {
+				t.Fatalf("Error starting server: %v", err)
+			}
+			defer ss.Stop()
 
-	client := testgrpc.NewTestServiceClient(cc)
+			cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(clientCreds))
+			if err != nil {
+				t.Fatalf("grpc.NewClient error: %v", err)
+			}
+			defer cc.Close()
 
-	const wantStr = "authentication handshake failed"
-	if _, err = client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.Unavailable || !strings.Contains(status.Convert(err).Message(), wantStr) {
-		t.Fatalf("EmptyCall err = %v; want code=%v, message contains %q", err, codes.Unavailable, wantStr)
+			client := testgrpc.NewTestServiceClient(cc)
+
+			const wantStr = "authentication handshake failed"
+			if _, err = client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.Unavailable || !strings.Contains(status.Convert(err).Message(), wantStr) {
+				t.Fatalf("EmptyCall err = %v; want code=%v, message contains %q", err, codes.Unavailable, wantStr)
+			}
+
+		})
 	}
 }
 
@@ -129,35 +172,78 @@ func (s) TestTLS_MinVersionOverridable(t *testing.T) {
 	for _, cs := range tls.CipherSuites() {
 		allCipherSuites = append(allCipherSuites, cs.ID)
 	}
-
-	// Create server creds that allow v1.0.
-	serverCreds := credentials.NewTLS(&tls.Config{
-		MinVersion:   tls.VersionTLS10,
-		Certificates: []tls.Certificate{serverCert},
-		CipherSuites: allCipherSuites,
-	})
-	ss := stubserver.StubServer{
-		EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
-			return &testpb.Empty{}, nil
+	testCases := []struct {
+		name      string
+		serverTLS func() *tls.Config
+	}{
+		{
+			name: "base_case",
+			serverTLS: func() *tls.Config {
+				return &tls.Config{
+					MinVersion:   tls.VersionTLS10,
+					Certificates: []tls.Certificate{serverCert},
+					CipherSuites: allCipherSuites,
+				}
+			},
+		},
+		{
+			name: "fallback_to_base",
+			serverTLS: func() *tls.Config {
+				config := &tls.Config{
+					MinVersion:   tls.VersionTLS10,
+					Certificates: []tls.Certificate{serverCert},
+					CipherSuites: allCipherSuites,
+				}
+				config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
+					return nil, nil
+				}
+				return config
+			},
+		},
+		{
+			name: "dynamic_using_get_config_for_client",
+			serverTLS: func() *tls.Config {
+				return &tls.Config{
+					GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
+						return &tls.Config{
+							MinVersion:   tls.VersionTLS10,
+							Certificates: []tls.Certificate{serverCert},
+							CipherSuites: allCipherSuites,
+						}, nil
+					},
+				}
+			},
 		},
 	}
 
-	// Create client creds that supports V1.0-V1.1.
-	clientCreds := credentials.NewTLS(&tls.Config{
-		ServerName:   serverName,
-		RootCAs:      certPool,
-		CipherSuites: allCipherSuites,
-		MinVersion:   tls.VersionTLS10,
-		MaxVersion:   tls.VersionTLS11,
-	})
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Create server creds that allow v1.0.
+			serverCreds := credentials.NewTLS(tc.serverTLS())
+			ss := stubserver.StubServer{
+				EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
+					return &testpb.Empty{}, nil
+				},
+			}
 
-	if err := ss.Start([]grpc.ServerOption{grpc.Creds(serverCreds)}, grpc.WithTransportCredentials(clientCreds)); err != nil {
-		t.Fatalf("Error starting stub server: %v", err)
-	}
-	defer ss.Stop()
+			// Create client creds that supports V1.0-V1.1.
+			clientCreds := credentials.NewTLS(&tls.Config{
+				ServerName:   serverName,
+				RootCAs:      certPool,
+				CipherSuites: allCipherSuites,
+				MinVersion:   tls.VersionTLS10,
+				MaxVersion:   tls.VersionTLS11,
+			})
 
-	if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
-		t.Fatalf("EmptyCall err = %v; want <nil>", err)
+			if err := ss.Start([]grpc.ServerOption{grpc.Creds(serverCreds)}, grpc.WithTransportCredentials(clientCreds)); err != nil {
+				t.Fatalf("Error starting stub server: %v", err)
+			}
+			defer ss.Stop()
+
+			if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
+				t.Fatalf("EmptyCall err = %v; want <nil>", err)
+			}
+		})
 	}
 }
 
@@ -165,43 +251,82 @@ func (s) TestTLS_MinVersionOverridable(t *testing.T) {
 func (s) TestTLS_CipherSuites(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
 	defer cancel()
-
-	// Create server creds without cipher suites.
-	serverCreds := credentials.NewTLS(&tls.Config{
-		Certificates: []tls.Certificate{serverCert},
-	})
-	ss := stubserver.StubServer{
-		EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
-			return &testpb.Empty{}, nil
+	testCases := []struct {
+		name      string
+		serverTLS func() *tls.Config
+	}{
+		{
+			name: "base_case",
+			serverTLS: func() *tls.Config {
+				return &tls.Config{
+					Certificates: []tls.Certificate{serverCert},
+				}
+			},
+		},
+		{
+			name: "fallback_to_base",
+			serverTLS: func() *tls.Config {
+				config := &tls.Config{
+					Certificates: []tls.Certificate{serverCert},
+				}
+				config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
+					return nil, nil
+				}
+				return config
+			},
+		},
+		{
+			name: "dynamic_using_get_config_for_client",
+			serverTLS: func() *tls.Config {
+				return &tls.Config{
+					GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
+						return &tls.Config{
+							Certificates: []tls.Certificate{serverCert},
+						}, nil
+					},
+				}
+			},
 		},
 	}
 
-	// Create client creds that use a forbidden suite only.
-	clientCreds := credentials.NewTLS(&tls.Config{
-		ServerName:   serverName,
-		RootCAs:      certPool,
-		CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
-		MaxVersion:   tls.VersionTLS12, // TLS1.3 cipher suites are not configurable, so limit to 1.2.
-	})
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Create server creds without cipher suites.
+			serverCreds := credentials.NewTLS(tc.serverTLS())
+			ss := stubserver.StubServer{
+				EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
+					return &testpb.Empty{}, nil
+				},
+			}
 
-	// Start server and client separately, because Start() blocks on a
-	// successful connection, which we will not get.
-	if err := ss.StartServer(grpc.Creds(serverCreds)); err != nil {
-		t.Fatalf("Error starting server: %v", err)
-	}
-	defer ss.Stop()
+			// Create client creds that use a forbidden suite only.
+			clientCreds := credentials.NewTLS(&tls.Config{
+				ServerName:   serverName,
+				RootCAs:      certPool,
+				CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
+				MaxVersion:   tls.VersionTLS12, // TLS1.3 cipher suites are not configurable, so limit to 1.2.
+			})
 
-	cc, err := grpc.NewClient("dns:"+ss.Address, grpc.WithTransportCredentials(clientCreds))
-	if err != nil {
-		t.Fatalf("grpc.NewClient error: %v", err)
-	}
-	defer cc.Close()
+			// Start server and client separately, because Start() blocks on a
+			// successful connection, which we will not get.
+			if err := ss.StartServer(grpc.Creds(serverCreds)); err != nil {
+				t.Fatalf("Error starting server: %v", err)
+			}
+			defer ss.Stop()
 
-	client := testgrpc.NewTestServiceClient(cc)
+			cc, err := grpc.NewClient("dns:"+ss.Address, grpc.WithTransportCredentials(clientCreds))
+			if err != nil {
+				t.Fatalf("grpc.NewClient error: %v", err)
+			}
+			defer cc.Close()
+
+			client := testgrpc.NewTestServiceClient(cc)
 
-	const wantStr = "authentication handshake failed"
-	if _, err = client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.Unavailable || !strings.Contains(status.Convert(err).Message(), wantStr) {
-		t.Fatalf("EmptyCall err = %v; want code=%v, message contains %q", err, codes.Unavailable, wantStr)
+			const wantStr = "authentication handshake failed"
+			if _, err = client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.Unavailable || !strings.Contains(status.Convert(err).Message(), wantStr) {
+				t.Fatalf("EmptyCall err = %v; want code=%v, message contains %q", err, codes.Unavailable, wantStr)
+			}
+		})
 	}
 }
 
@@ -210,23 +335,108 @@ func (s) TestTLS_CipherSuitesOverridable(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
 	defer cancel()
 
-	// Create server that allows only a forbidden cipher suite.
+	testCases := []struct {
+		name      string
+		serverTLS func() *tls.Config
+	}{
+		{
+			name: "base_case",
+			serverTLS: func() *tls.Config {
+				return &tls.Config{
+					Certificates: []tls.Certificate{serverCert},
+					CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
+				}
+			},
+		},
+		{
+			name: "fallback_to_base",
+			serverTLS: func() *tls.Config {
+				config := &tls.Config{
+					Certificates: []tls.Certificate{serverCert},
+					CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
+				}
+				config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
+					return nil, nil
+				}
+				return config
+			},
+		},
+		{
+			name: "dynamic_using_get_config_for_client",
+			serverTLS: func() *tls.Config {
+				return &tls.Config{
+					GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
+						return &tls.Config{
+							Certificates: []tls.Certificate{serverCert},
+							CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
+						}, nil
+					},
+				}
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Create server that allows only a forbidden cipher suite.
+			serverCreds := credentials.NewTLS(tc.serverTLS())
+			ss := stubserver.StubServer{
+				EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
+					return &testpb.Empty{}, nil
+				},
+			}
+
+			// Create server that allows only a forbidden cipher suite.
+			clientCreds := credentials.NewTLS(&tls.Config{
+				ServerName:   serverName,
+				RootCAs:      certPool,
+				CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
+				MaxVersion:   tls.VersionTLS12, // TLS1.3 cipher suites are not configurable, so limit to 1.2.
+			})
+
+			if err := ss.Start([]grpc.ServerOption{grpc.Creds(serverCreds)}, grpc.WithTransportCredentials(clientCreds)); err != nil {
+				t.Fatalf("Error starting stub server: %v", err)
+			}
+			defer ss.Stop()
+
+			if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
+				t.Fatalf("EmptyCall err = %v; want <nil>", err)
+			}
+		})
+	}
+}
+
+// TestTLS_ServerConfiguresALPNByDefault verifies that ALPN is configured
+// correctly for a server that doesn't specify the NextProtos field and uses
+// GetConfigForClient to provide the TLS config during the handshake.
+func (s) TestTLS_ServerConfiguresALPNByDefault(t *testing.T) {
+	initialVal := envconfig.EnforceALPNEnabled
+	defer func() {
+		envconfig.EnforceALPNEnabled = initialVal
+	}()
+	envconfig.EnforceALPNEnabled = true
+
+	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
+	defer cancel()
+
+	// Create a server that doesn't set the NextProtos field.
 	serverCreds := credentials.NewTLS(&tls.Config{
-		Certificates: []tls.Certificate{serverCert},
-		CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
+		GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
+			return &tls.Config{
+				Certificates: []tls.Certificate{serverCert},
+			}, nil
+		},
 	})
+
 	ss := stubserver.StubServer{
 		EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
 			return &testpb.Empty{}, nil
 		},
 	}
 
-	// Create server that allows only a forbidden cipher suite.
 	clientCreds := credentials.NewTLS(&tls.Config{
-		ServerName:   serverName,
-		RootCAs:      certPool,
-		CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
-		MaxVersion:   tls.VersionTLS12, // TLS1.3 cipher suites are not configurable, so limit to 1.2.
+		ServerName: serverName,
+		RootCAs:    certPool,
 	})
 
 	if err := ss.Start([]grpc.ServerOption{grpc.Creds(serverCreds)}, grpc.WithTransportCredentials(clientCreds)); err != nil {
diff --git a/reflection/test/go.mod b/reflection/test/go.mod
deleted file mode 100644
index 7382e0e9..00000000
--- a/reflection/test/go.mod
+++ /dev/null
@@ -1,18 +0,0 @@
-module google.golang.org/grpc/reflection/test
-
-go 1.22.7
-
-replace google.golang.org/grpc => ../../
-
-require (
-	github.com/golang/protobuf v1.5.4
-	google.golang.org/grpc v1.67.1
-	google.golang.org/protobuf v1.35.1
-)
-
-require (
-	golang.org/x/net v0.30.0 // indirect
-	golang.org/x/sys v0.26.0 // indirect
-	golang.org/x/text v0.19.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
-)
diff --git a/reflection/test/go.sum b/reflection/test/go.sum
deleted file mode 100644
index c1616371..00000000
--- a/reflection/test/go.sum
+++ /dev/null
@@ -1,14 +0,0 @@
-github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
-golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
-golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
-golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
-golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
-google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
-google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
diff --git a/reflection/test/serverreflection_test.go b/reflection/test/serverreflection_test.go
index 29698c60..0a436f2a 100644
--- a/reflection/test/serverreflection_test.go
+++ b/reflection/test/serverreflection_test.go
@@ -43,7 +43,7 @@ import (
 	v1reflectionpb "google.golang.org/grpc/reflection/grpc_reflection_v1"
 	v1alphareflectiongrpc "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
 	pb "google.golang.org/grpc/reflection/grpc_testing"
-	pbv3 "google.golang.org/grpc/reflection/test/grpc_testing_not_regenerate"
+	pbv3 "google.golang.org/grpc/testdata/grpc_testing_not_regenerated"
 )
 
 var (
diff --git a/status/status_ext_test.go b/status/status_ext_test.go
index 659d10d0..118f6789 100644
--- a/status/status_ext_test.go
+++ b/status/status_ext_test.go
@@ -21,6 +21,7 @@ package status_test
 import (
 	"context"
 	"errors"
+	"reflect"
 	"strings"
 	"testing"
 	"time"
@@ -35,8 +36,10 @@ import (
 	"google.golang.org/grpc/status"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/protoadapt"
+	"google.golang.org/protobuf/testing/protocmp"
 
 	testpb "google.golang.org/grpc/interop/grpc_testing"
+	tpb "google.golang.org/grpc/testdata/grpc_testing_not_regenerated"
 )
 
 const defaultTestTimeout = 10 * time.Second
@@ -203,3 +206,56 @@ func (s) TestStatusDetails(t *testing.T) {
 		})
 	}
 }
+
+// TestStatus_ErrorDetailsMessageV1 verifies backward compatibility of the
+// status.Details() method when using protobuf code generated with only the
+// MessageV1 API implementation.
+func (s) TestStatus_ErrorDetailsMessageV1(t *testing.T) {
+	details := []protoadapt.MessageV1{
+		&tpb.SimpleMessage{Data: "abc"},
+	}
+	s, err := status.New(codes.Aborted, "").WithDetails(details...)
+	if err != nil {
+		t.Fatalf("(%v).WithDetails(%+v) failed: %v", s, details, err)
+	}
+	gotDetails := s.Details()
+	for i, msg := range gotDetails {
+		if got, want := reflect.TypeOf(msg), reflect.TypeOf(details[i]); got != want {
+			t.Errorf("reflect.Typeof(%v) = %v, want = %v", msg, got, want)
+		}
+		if _, ok := msg.(protoadapt.MessageV1); !ok {
+			t.Errorf("(%v).Details() returned message that doesn't implement protoadapt.MessageV1: %v", s, msg)
+		}
+		if diff := cmp.Diff(msg, details[i], protocmp.Transform()); diff != "" {
+			t.Errorf("(%v).Details got unexpected output, diff (-got +want):\n%s", s, diff)
+		}
+	}
+}
+
+// TestStatus_ErrorDetailsMessageV1AndV2 verifies that status.Details() method
+// returns the same message types when using protobuf code generated with both the
+// MessageV1 and MessageV2 API implementations.
+func (s) TestStatus_ErrorDetailsMessageV1AndV2(t *testing.T) {
+	details := []protoadapt.MessageV1{
+		&testpb.Empty{},
+	}
+	s, err := status.New(codes.Aborted, "").WithDetails(details...)
+	if err != nil {
+		t.Fatalf("(%v).WithDetails(%+v) failed: %v", s, details, err)
+	}
+	gotDetails := s.Details()
+	for i, msg := range gotDetails {
+		if got, want := reflect.TypeOf(msg), reflect.TypeOf(details[i]); got != want {
+			t.Errorf("reflect.Typeof(%v) = %v, want = %v", msg, got, want)
+		}
+		if _, ok := msg.(protoadapt.MessageV1); !ok {
+			t.Errorf("(%v).Details() returned message that doesn't implement protoadapt.MessageV1: %v", s, msg)
+		}
+		if _, ok := msg.(protoadapt.MessageV2); !ok {
+			t.Errorf("(%v).Details() returned message that doesn't implement protoadapt.MessageV2: %v", s, msg)
+		}
+		if diff := cmp.Diff(msg, details[i], protocmp.Transform()); diff != "" {
+			t.Errorf("(%v).Details got unexpected output, diff (-got +want):\n%s", s, diff)
+		}
+	}
+}
diff --git a/reflection/test/grpc_testing_not_regenerate/README.md b/testdata/grpc_testing_not_regenerated/README.md
similarity index 69%
rename from reflection/test/grpc_testing_not_regenerate/README.md
rename to testdata/grpc_testing_not_regenerated/README.md
index 34e16466..8ff72f05 100644
--- a/reflection/test/grpc_testing_not_regenerate/README.md
+++ b/testdata/grpc_testing_not_regenerated/README.md
@@ -6,3 +6,7 @@ with `"context"`.
 
 `dynamic.go` was generated with a newer protoc and manually edited to remove
 everything except the descriptor bytes var, which is renamed and exported.
+
+`simple_message_v1.go` was generated using protoc-gen-go v1.3.5 which doesn't
+support the MesssageV2 API. As a result the generated code implements only the
+old MessageV1 API.
diff --git a/reflection/test/grpc_testing_not_regenerate/dynamic.go b/testdata/grpc_testing_not_regenerated/dynamic.go
similarity index 98%
rename from reflection/test/grpc_testing_not_regenerate/dynamic.go
rename to testdata/grpc_testing_not_regenerated/dynamic.go
index 35e4f024..e75dbea7 100644
--- a/reflection/test/grpc_testing_not_regenerate/dynamic.go
+++ b/testdata/grpc_testing_not_regenerated/dynamic.go
@@ -15,7 +15,7 @@
  *
  */
 
-package grpc_testing_not_regenerate
+package grpc_testing_not_regenerated
 
 // FileDynamicProtoRawDesc is the descriptor for dynamic.proto, see README.md.
 var FileDynamicProtoRawDesc = []byte{
diff --git a/reflection/test/grpc_testing_not_regenerate/dynamic.proto b/testdata/grpc_testing_not_regenerated/dynamic.proto
similarity index 91%
rename from reflection/test/grpc_testing_not_regenerate/dynamic.proto
rename to testdata/grpc_testing_not_regenerated/dynamic.proto
index 6b6b0a72..72e49156 100644
--- a/reflection/test/grpc_testing_not_regenerate/dynamic.proto
+++ b/testdata/grpc_testing_not_regenerated/dynamic.proto
@@ -17,10 +17,10 @@
 
 syntax = "proto3";
 
-option go_package = "google.golang.org/grpc/reflection/test/grpc_testing_not_regenerate";
-
 package grpc.testing;
 
+option go_package = "google.golang.org/grpc/testdata/grpc_testing_not_regenerated";
+
 message DynamicRes {}
 
 message DynamicReq {}
diff --git a/testdata/grpc_testing_not_regenerated/simple.proto b/testdata/grpc_testing_not_regenerated/simple.proto
new file mode 100644
index 00000000..c8b13c77
--- /dev/null
+++ b/testdata/grpc_testing_not_regenerated/simple.proto
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+syntax = "proto3";
+
+package grpc.testdata.grpc_testing_not_regenerated;
+
+option go_package = "google.golang.org/grpc/testdata/grpc_testing_not_regenerated";
+
+// SimpleMessage is used to hold string data.
+message SimpleMessage {
+  string data = 1;
+}
diff --git a/testdata/grpc_testing_not_regenerated/simple_message_v1.go b/testdata/grpc_testing_not_regenerated/simple_message_v1.go
new file mode 100644
index 00000000..7bf9ee2b
--- /dev/null
+++ b/testdata/grpc_testing_not_regenerated/simple_message_v1.go
@@ -0,0 +1,83 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: simple.proto
+
+package grpc_testing_not_regenerated
+
+import (
+	fmt "fmt"
+	math "math"
+
+	proto "github.com/golang/protobuf/proto"
+)
+
+// 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.ProtoPackageIsVersion3 // please upgrade the proto package
+
+// SimpleMessage is used to hold string data.
+type SimpleMessage struct {
+	Data                 string   `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *SimpleMessage) Reset()         { *m = SimpleMessage{} }
+func (m *SimpleMessage) String() string { return proto.CompactTextString(m) }
+func (*SimpleMessage) ProtoMessage()    {}
+func (*SimpleMessage) Descriptor() ([]byte, []int) {
+	return fileDescriptor_5ffd045dd4d042c1, []int{0}
+}
+
+func (m *SimpleMessage) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_SimpleMessage.Unmarshal(m, b)
+}
+func (m *SimpleMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_SimpleMessage.Marshal(b, m, deterministic)
+}
+func (m *SimpleMessage) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_SimpleMessage.Merge(m, src)
+}
+func (m *SimpleMessage) XXX_Size() int {
+	return xxx_messageInfo_SimpleMessage.Size(m)
+}
+func (m *SimpleMessage) XXX_DiscardUnknown() {
+	xxx_messageInfo_SimpleMessage.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_SimpleMessage proto.InternalMessageInfo
+
+func (m *SimpleMessage) GetData() string {
+	if m != nil {
+		return m.Data
+	}
+	return ""
+}
+
+func init() {
+	proto.RegisterType((*SimpleMessage)(nil), "grpc.testdata.grpc_testing_not_regenerated.SimpleMessage")
+}
+
+func init() {
+	proto.RegisterFile("simple.proto", fileDescriptor_5ffd045dd4d042c1)
+}
+
+var fileDescriptor_5ffd045dd4d042c1 = []byte{
+	// 142 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x29, 0xce, 0xcc, 0x2d,
+	0xc8, 0x49, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xd2, 0x4a, 0x2f, 0x2a, 0x48, 0xd6, 0x2b,
+	0x49, 0x2d, 0x2e, 0x49, 0x49, 0x2c, 0x49, 0xd4, 0x03, 0xf1, 0xe2, 0x41, 0xbc, 0xcc, 0xbc, 0xf4,
+	0xf8, 0xbc, 0xfc, 0x92, 0xf8, 0xa2, 0xd4, 0xf4, 0xd4, 0xbc, 0xd4, 0xa2, 0xc4, 0x92, 0xd4, 0x14,
+	0x25, 0x65, 0x2e, 0xde, 0x60, 0xb0, 0x5e, 0xdf, 0xd4, 0xe2, 0xe2, 0xc4, 0xf4, 0x54, 0x21, 0x21,
+	0x2e, 0x16, 0x90, 0x2e, 0x09, 0x46, 0x05, 0x46, 0x0d, 0xce, 0x20, 0x30, 0xdb, 0xc9, 0x2e, 0xca,
+	0x26, 0x3d, 0x3f, 0x3f, 0x3d, 0x27, 0x55, 0x2f, 0x3d, 0x3f, 0x27, 0x31, 0x2f, 0x5d, 0x2f, 0xbf,
+	0x28, 0x5d, 0x1f, 0x64, 0xac, 0x3e, 0xcc, 0x12, 0x7d, 0x7c, 0x96, 0x24, 0xb1, 0x81, 0xdd, 0x65,
+	0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x2c, 0x80, 0xd6, 0x07, 0xa7, 0x00, 0x00, 0x00,
+}
diff --git a/reflection/test/grpc_testing_not_regenerate/testv3.go b/testdata/grpc_testing_not_regenerated/testv3.go
similarity index 99%
rename from reflection/test/grpc_testing_not_regenerate/testv3.go
rename to testdata/grpc_testing_not_regenerated/testv3.go
index 8a690963..7c55c9ad 100644
--- a/reflection/test/grpc_testing_not_regenerate/testv3.go
+++ b/testdata/grpc_testing_not_regenerated/testv3.go
@@ -20,7 +20,7 @@
 // DO NOT EDIT!
 
 /*
-Package grpc_testing_not_regenerate is a generated protocol buffer package.
+Package grpc_testing_not_regenerated is a generated protocol buffer package.
 
 It is generated from these files:
 
@@ -31,7 +31,7 @@ It has these top-level messages:
 	SearchResponseV3
 	SearchRequestV3
 */
-package grpc_testing_not_regenerate
+package grpc_testing_not_regenerated
 
 import (
 	context "context"
diff --git a/reflection/test/grpc_testing_not_regenerate/testv3.proto b/testdata/grpc_testing_not_regenerated/testv3.proto
similarity index 93%
rename from reflection/test/grpc_testing_not_regenerate/testv3.proto
rename to testdata/grpc_testing_not_regenerated/testv3.proto
index 9902ad67..c8eee856 100644
--- a/reflection/test/grpc_testing_not_regenerate/testv3.proto
+++ b/testdata/grpc_testing_not_regenerated/testv3.proto
@@ -17,10 +17,10 @@
 
 syntax = "proto3";
 
-option go_package = "google.golang.org/grpc/reflection/test/grpc_testing_not_regenerate";
-
 package grpc.testingv3;
 
+option go_package = "google.golang.org/grpc/testdata/grpc_testing_not_regenerated";
+
 message SearchResponseV3 {
   message Result {
     string url = 1;