This is an automated email from the ASF dual-hosted git repository.

Alanxtl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git


The following commit(s) were added to refs/heads/develop by this push:
     new 3996cc839 feat(triple): dispatch non-IDL wrapper decoding by 
SerializeType (#3550)
3996cc839 is described below

commit 3996cc839bc827c48c1b1e24e5f098825784bd17
Author: eye-gu <[email protected]>
AuthorDate: Tue Aug 18 10:47:28 2026 +0800

    feat(triple): dispatch non-IDL wrapper decoding by SerializeType (#3550)
    
    * feat(triple): dispatch non-IDL wrapper decoding by SerializeType
    
    * fix(triple): normalize hessian4 serialize type for Java interop
    
    * harden non-IDL inner codec registry and add serialize-type allowlist
    
    * fix(triple): unwrap response container and validate SerializeType for 
empty Data
    
    * fix format
---
 protocol/triple/triple_protocol/codec.go           | 204 ++++++---
 protocol/triple/triple_protocol/codec_test.go      |  45 ++
 .../triple/triple_protocol/codec_wrapper_test.go   | 481 ++++++++++++++++++++-
 protocol/triple/triple_protocol/inner_codec.go     |  88 ++++
 protocol/triple/triple_protocol/protocol_triple.go |  39 +-
 5 files changed, 760 insertions(+), 97 deletions(-)

diff --git a/protocol/triple/triple_protocol/codec.go 
b/protocol/triple/triple_protocol/codec.go
index 66768438c..5b798ac47 100644
--- a/protocol/triple/triple_protocol/codec.go
+++ b/protocol/triple/triple_protocol/codec.go
@@ -100,10 +100,10 @@ type stableCodec interface {
        IsBinary() bool
 }
 
-// protoBinaryCodec handles standard protobuf binary serialization.
-// It also supports Java Dubbo Triple generic calls when the message is not a 
proto.Message.
-// This dual functionality is needed because the server receives wrapped 
generic calls
-// with Content-Type "application/proto", so this codec must handle both cases.
+// protoBinaryCodec handles standard protobuf binary serialization for IDL
+// calls. Non-IDL (Java Dubbo Triple generic call) wrapper handling on the
+// server side is handled by tripleServerCodecSession, which delegates to
+// this codec for the IDL path.
 type protoBinaryCodec struct{}
 
 var _ Codec = (*protoBinaryCodec)(nil)
@@ -121,61 +121,11 @@ func (c *protoBinaryCodec) Marshal(message any) ([]byte, 
error) {
 func (c *protoBinaryCodec) Unmarshal(data []byte, message any) error {
        protoMessage, ok := message.(proto.Message)
        if !ok {
-               // Non-proto types indicate a generic call - try to unwrap from 
wrapper format.
-               // This is used by the server when receiving Java/Go generic 
calls.
-               return c.unmarshalWrappedMessage(data, message)
+               return errNotProto(message)
        }
        return proto.Unmarshal(data, protoMessage)
 }
 
-// unmarshalWrappedMessage handles both TripleResponseWrapper and 
TripleRequestWrapper formats.
-// It determines the format by checking if message is a slice (request) or not 
(response).
-func (c *protoBinaryCodec) unmarshalWrappedMessage(data []byte, message any) 
error {
-       hessianCodec := &hessian2Codec{}
-
-       // Check if message is a slice - if so, it's a request with multiple 
args
-       if params, isSlice := message.([]any); isSlice {
-               // Request format: TripleRequestWrapper with multiple args
-               var reqWrapper interoperability.TripleRequestWrapper
-               if err := proto.Unmarshal(data, &reqWrapper); err != nil {
-                       return fmt.Errorf("unmarshal wrapped request: %w", err)
-               }
-               if len(reqWrapper.Args) != len(params) {
-                       return fmt.Errorf("unmarshal wrapped request: expected 
%d params, got %d args", len(params), len(reqWrapper.Args))
-               }
-
-               for i, arg := range reqWrapper.Args {
-                       if err := hessianCodec.Unmarshal(arg, params[i]); err 
!= nil {
-                               return fmt.Errorf("unmarshal wrapped request 
arg[%d]: %w", i, err)
-                       }
-               }
-               return nil
-       }
-
-       // Response format: TripleResponseWrapper with single data field
-       var respWrapper interoperability.TripleResponseWrapper
-       if err := proto.Unmarshal(data, &respWrapper); err == nil {
-               // Check if it's a valid response wrapper (has serializeType or 
non-empty data)
-               if len(respWrapper.Data) > 0 {
-                       return hessianCodec.Unmarshal(respWrapper.Data, message)
-               }
-               // Empty Data with serializeType indicates a null/void 
response, which is valid
-               if respWrapper.SerializeType != "" {
-                       return nil
-               }
-       }
-
-       // Fallback: try as single-arg request (not a response wrapper)
-       var reqWrapper interoperability.TripleRequestWrapper
-       if err := proto.Unmarshal(data, &reqWrapper); err != nil {
-               return fmt.Errorf("unmarshal wrapped message: %T is not a 
proto.Message and data is not a valid wrapper", message)
-       }
-       if len(reqWrapper.Args) != 1 {
-               return fmt.Errorf("unmarshal wrapped message: expected 1 arg 
for single param, got %d", len(reqWrapper.Args))
-       }
-       return hessianCodec.Unmarshal(reqWrapper.Args[0], message)
-}
-
 func (c *protoBinaryCodec) MarshalStable(message any) ([]byte, error) {
        protoMessage, ok := message.(proto.Message)
        if !ok {
@@ -338,8 +288,12 @@ func (c *protoWrapperCodec) Unmarshal(binary []byte, 
message any) error {
                        return fmt.Errorf("wrapper codec: expected %d params, 
got %d args", len(params), len(wrapperReq.Args))
                }
 
+               inner, err := resolveInnerCodec(wrapperReq.SerializeType)
+               if err != nil {
+                       return fmt.Errorf("wrapper codec: %w", err)
+               }
                for i, arg := range wrapperReq.Args {
-                       if err := c.innerCodec.Unmarshal(arg, params[i]); err 
!= nil {
+                       if err := inner.Unmarshal(arg, params[i]); err != nil {
                                return err
                        }
                }
@@ -349,14 +303,16 @@ func (c *protoWrapperCodec) Unmarshal(binary []byte, 
message any) error {
        // Response format: TripleResponseWrapper with single data field
        var wrapperResp interoperability.TripleResponseWrapper
        if err := proto.Unmarshal(binary, &wrapperResp); err == nil {
-               // Check if it's a valid response wrapper (has serializeType or 
non-empty data)
-               if len(wrapperResp.Data) > 0 {
-                       return c.innerCodec.Unmarshal(wrapperResp.Data, message)
+               inner, err := resolveInnerCodec(wrapperResp.SerializeType)
+               if err != nil {
+                       return fmt.Errorf("wrapper codec: %w", err)
                }
-               // Empty Data with serializeType indicates a null/void 
response, which is valid
-               if wrapperResp.SerializeType != "" {
-                       return nil
+               // Non-empty Data: decode the single return value.
+               if len(wrapperResp.Data) > 0 {
+                       return inner.Unmarshal(wrapperResp.Data, message)
                }
+               // Empty Data with a validated SerializeType is a null/void 
response.
+               return nil
        }
 
        // Fallback: try as single-arg request (not a response wrapper)
@@ -367,7 +323,11 @@ func (c *protoWrapperCodec) Unmarshal(binary []byte, 
message any) error {
        if len(wrapperReq.Args) != 1 {
                return fmt.Errorf("wrapper codec: expected 1 arg for single 
param, got %d", len(wrapperReq.Args))
        }
-       return c.innerCodec.Unmarshal(wrapperReq.Args[0], message)
+       inner, err := resolveInnerCodec(wrapperReq.SerializeType)
+       if err != nil {
+               return fmt.Errorf("wrapper codec: %w", err)
+       }
+       return inner.Unmarshal(wrapperReq.Args[0], message)
 }
 
 func newProtoWrapperCodec(innerCodec Codec) *protoWrapperCodec {
@@ -619,6 +579,124 @@ func copySlice(inSlice, outSlice reflect.Value) error {
        return nil
 }
 
+// tripleServerCodecSession is a per-request Codec for the triple server that
+// handles both IDL and Non-IDL formats.
+//
+// SerializeType is request-scoped state that the Codec interface
+// (Marshal/Unmarshal) has no channel to surface. The session object IS that
+// channel: Unmarshal captures SerializeType from the TripleRequestWrapper in a
+// single decode, and Marshal reads it to wrap the response in a
+// TripleResponseWrapper.
+type tripleServerCodecSession struct {
+       delegate             Codec  // IDL path codec, resolved from 
Content-Type
+       serializeType        string // captured by Unmarshal when Non-IDL; read 
by Marshal
+       allowedSerializeType string // provider "serialization" param; 
effective allowlist = {hessian2} ∪ {this}. TODO: support Java's multi-valued 
prefer-serialization
+}
+
+var _ Codec = (*tripleServerCodecSession)(nil)
+
+func (s *tripleServerCodecSession) Name() string { return s.delegate.Name() }
+
+// checkAllowed enforces the provider-side serialization allowlist.
+// hessian2 is always allowed (Non-IDL interop default); any other name must
+// match the provider's configured serialization.
+func (s *tripleServerCodecSession) checkAllowed(codecName string) error {
+       if codecName == codecNameHessian2 || codecName == 
s.allowedSerializeType {
+               return nil
+       }
+       return fmt.Errorf("serialize type %q not allowed by provider (allowed: 
%s, %s)",
+               codecName, codecNameHessian2, s.allowedSerializeType)
+}
+
+func (s *tripleServerCodecSession) Unmarshal(data []byte, message any) error {
+       if _, isProto := message.(proto.Message); isProto {
+               // IDL: standard proto message.
+               return s.delegate.Unmarshal(data, message)
+       }
+       // Non-IDL: decode the TripleRequestWrapper once, capturing 
SerializeType
+       // for the subsequent response Marshal and decoding the inner args in 
the
+       // same pass.
+       var reqWrapper interoperability.TripleRequestWrapper
+       if err := proto.Unmarshal(data, &reqWrapper); err != nil {
+               return fmt.Errorf("unmarshal triple wrapper request: %w", err)
+       }
+       s.serializeType = reqWrapper.SerializeType
+       inner, err := resolveInnerCodec(reqWrapper.SerializeType)
+       if err != nil {
+               return fmt.Errorf("unmarshal triple wrapper request: %w", err)
+       }
+       if err := s.checkAllowed(inner.Name()); err != nil {
+               return fmt.Errorf("unmarshal triple wrapper request: %w", err)
+       }
+       return unmarshalWrapperRequestArgs(&reqWrapper, inner, message)
+}
+
+func (s *tripleServerCodecSession) Marshal(message any) ([]byte, error) {
+       if _, isProto := message.(proto.Message); isProto {
+               // IDL: standard proto message.
+               return s.delegate.Marshal(message)
+       }
+       // Non-IDL: wrap the response in a TripleResponseWrapper whose Data is
+       // serialized with the inner codec resolved from the request's 
SerializeType.
+       inner, err := resolveInnerCodec(s.serializeType)
+       if err != nil {
+               return nil, fmt.Errorf("marshal triple wrapper response: %w", 
err)
+       }
+       payload := message
+       var isVoid bool
+       if container, ok := message.([]any); ok {
+               // The production handler packs exactly one return value as
+               // []any{result} (server.go wrapTripleResponse). More elements 
indicate
+               // a programming error; fail loudly instead of silently 
truncating.
+               switch len(container) {
+               case 0:
+                       isVoid = true
+               case 1:
+                       payload = container[0]
+                       if payload == nil {
+                               isVoid = true
+                       }
+               default:
+                       return nil, fmt.Errorf("marshal triple wrapper 
response: expected at most 1 return value, got %d", len(container))
+               }
+       }
+       var data []byte
+       if !isVoid {
+               data, err = inner.Marshal(payload)
+               if err != nil {
+                       return nil, fmt.Errorf("marshal triple wrapper response 
data: %w", err)
+               }
+       }
+       // Use inner.Name() instead of s.serializeType so that an absent 
SerializeType
+       // (defaulted to hessian2 by resolveInnerCodec) is normalized on the 
wire.
+       return proto.Marshal(&interoperability.TripleResponseWrapper{
+               SerializeType: inner.Name(),
+               Data:          data,
+       })
+}
+
+// unmarshalWrapperRequestArgs decodes the inner args of a TripleRequestWrapper
+// into message. message may be []any (multi-arg generic call) or a single
+// value (single-arg call packed as a one-element wrapper).
+func unmarshalWrapperRequestArgs(w *interoperability.TripleRequestWrapper, 
inner Codec, message any) error {
+       if params, isSlice := message.([]any); isSlice {
+               if len(w.Args) != len(params) {
+                       return fmt.Errorf("triple wrapper request: expected %d 
params, got %d args", len(params), len(w.Args))
+               }
+               for i, arg := range w.Args {
+                       if err := inner.Unmarshal(arg, params[i]); err != nil {
+                               return fmt.Errorf("triple wrapper request 
arg[%d]: %w", i, err)
+                       }
+               }
+               return nil
+       }
+       // Single-arg call: the wrapper carries one arg decoded into message.
+       if len(w.Args) != 1 {
+               return fmt.Errorf("triple wrapper request: expected 1 arg for 
single param, got %d", len(w.Args))
+       }
+       return inner.Unmarshal(w.Args[0], message)
+}
+
 // copyMap copy from in map to out map
 func copyMap(inMapValue, outMapValue reflect.Value) error {
        if inMapValue.IsNil() {
diff --git a/protocol/triple/triple_protocol/codec_test.go 
b/protocol/triple/triple_protocol/codec_test.go
index b9b1dc025..d03491bac 100644
--- a/protocol/triple/triple_protocol/codec_test.go
+++ b/protocol/triple/triple_protocol/codec_test.go
@@ -346,3 +346,48 @@ func TestHessian2Codec(t *testing.T) {
                var _ Codec = (*hessian2Codec)(nil)
        })
 }
+
+func TestResolveInnerCodec(t *testing.T) {
+       t.Parallel()
+       cases := []struct {
+               name          string
+               serializeType string
+               wantOK        bool
+               // wantName is checked only when wantOK is true. Empty means 
skip the
+               // name assertion (kept simple for cases that only care about 
resolve).
+               wantName string
+       }{
+               {"hessian2", "hessian2", true, "hessian2"},
+               {"msgpack", "msgpack", true, "msgpack"},
+               {"empty-defaults-hessian2", "", true, "hessian2"},
+               // Dubbo Java writes "hessian4" into the wrapper 
(TripleConstants.HESSIAN4);
+               // it denotes the same on-wire Hessian2 encoding and must 
resolve to the
+               // hessian2 codec. Mirrors Java's 
ReflectionPackableMethod.convertHessianFromWrapper.
+               {"hessian4-alias", "hessian4", true, "hessian2"},
+               {"unknown", "unknown", false, ""},
+               // Bare "hessian" is not a value Dubbo writes; it must be 
rejected rather
+               // than silently normalized, so misbehaving peers surface 
clearly.
+               {"hessian-not-aliased", "hessian", false, ""},
+       }
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       s, err := resolveInnerCodec(tc.serializeType)
+                       if tc.wantOK {
+                               if err != nil {
+                                       t.Fatalf("resolveInnerCodec(%q) err: 
%v", tc.serializeType, err)
+                               }
+                               if s == nil {
+                                       t.Fatalf("got nil serializer")
+                               }
+                               if tc.wantName != "" && s.Name() != tc.wantName 
{
+                                       t.Fatalf("resolveInnerCodec(%q) name = 
%q, want %q",
+                                               tc.serializeType, s.Name(), 
tc.wantName)
+                               }
+                       } else {
+                               if err == nil {
+                                       t.Fatalf("expected error for %q, got 
nil", tc.serializeType)
+                               }
+                       }
+               })
+       }
+}
diff --git a/protocol/triple/triple_protocol/codec_wrapper_test.go 
b/protocol/triple/triple_protocol/codec_wrapper_test.go
index defb837c7..903c3257a 100644
--- a/protocol/triple/triple_protocol/codec_wrapper_test.go
+++ b/protocol/triple/triple_protocol/codec_wrapper_test.go
@@ -18,6 +18,10 @@
 package triple_protocol
 
 import (
+       "context"
+       "net/http"
+       "net/http/httptest"
+       "strings"
        "testing"
        "time"
 )
@@ -240,10 +244,11 @@ func TestProtoBinaryCodec_MarshalNonProtoReturnsError(t 
*testing.T) {
        assert.NotNil(t, err)
 }
 
-func TestProtoBinaryCodec_UnmarshalWrappedResponse(t *testing.T) {
+func TestProtoWrapperCodec_UnmarshalWrappedResponse(t *testing.T) {
        t.Parallel()
 
-       codec := &protoBinaryCodec{}
+       // Client-side: protoWrapperCodec decodes a TripleResponseWrapper.
+       codec := newProtoWrapperCodec(&hessian2Codec{})
 
        // Create a TripleResponseWrapper
        hessianCodec := &hessian2Codec{}
@@ -263,10 +268,12 @@ func TestProtoBinaryCodec_UnmarshalWrappedResponse(t 
*testing.T) {
        assert.Equal(t, result, "hello world")
 }
 
-func TestProtoBinaryCodec_UnmarshalWrappedRequest(t *testing.T) {
+func TestServerCodecSession_UnmarshalWrappedRequest(t *testing.T) {
        t.Parallel()
 
-       codec := &protoBinaryCodec{}
+       // Server-side: tripleServerCodecSession decodes a TripleRequestWrapper 
and
+       // captures SerializeType for the subsequent response Marshal.
+       codecSession := &tripleServerCodecSession{delegate: &protoBinaryCodec{}}
 
        // Create a TripleRequestWrapper
        hessianCodec := &hessian2Codec{}
@@ -286,7 +293,7 @@ func TestProtoBinaryCodec_UnmarshalWrappedRequest(t 
*testing.T) {
                var v any
                results[i] = &v
        }
-       err := codec.Unmarshal(data, results)
+       err := codecSession.Unmarshal(data, results)
        assert.Nil(t, err)
 
        // Verify the unmarshaled values
@@ -294,12 +301,13 @@ func TestProtoBinaryCodec_UnmarshalWrappedRequest(t 
*testing.T) {
        val1 := *(results[1].(*any))
        assert.Equal(t, val0, "arg1")
        assert.Equal(t, val1, int64(123))
+       assert.Equal(t, codecSession.serializeType, codecNameHessian2)
 }
 
-func TestProtoBinaryCodec_ResponseThenRequestFallback(t *testing.T) {
+func TestServerCodecSession_UnmarshalSingleArgRequest(t *testing.T) {
        t.Parallel()
 
-       codec := &protoBinaryCodec{}
+       session := &tripleServerCodecSession{delegate: &protoBinaryCodec{}}
 
        // Test that it tries TripleResponseWrapper first, then falls back to 
TripleRequestWrapper
        // Create a valid TripleRequestWrapper
@@ -318,7 +326,7 @@ func TestProtoBinaryCodec_ResponseThenRequestFallback(t 
*testing.T) {
        results := make([]any, 1)
        var v any
        results[0] = &v
-       err := codec.Unmarshal(data, results)
+       err := session.Unmarshal(data, results)
        assert.Nil(t, err)
        assert.Equal(t, *(results[0].(*any)), "test")
 }
@@ -626,3 +634,460 @@ func TestProtoWrapperCodec_Msgpack(t *testing.T) {
        assert.Equal(t, codec.Name(), codecNameMsgPack)
        assert.Equal(t, codec.WireCodecName(), codecNameProto)
 }
+
+func TestServerCodecSession_UnmarshalWrappedRequest_MsgPack(t *testing.T) {
+       t.Parallel()
+
+       // Provider configured serialization=msgpack, so msgpack is on the 
allowlist.
+       session := &tripleServerCodecSession{
+               delegate:             &protoBinaryCodec{},
+               allowedSerializeType: codecNameMsgPack,
+       }
+
+       msgpCodec := &msgpackCodec{}
+       arg1, _ := msgpCodec.Marshal("msgarg")
+       arg2, _ := msgpCodec.Marshal(int64(7))
+
+       wrapper := &interoperability.TripleRequestWrapper{
+               SerializeType: codecNameMsgPack,
+               Args:          [][]byte{arg1, arg2},
+               ArgTypes:      []string{"java.lang.String", "long"},
+       }
+       data, _ := proto.Marshal(wrapper)
+
+       results := make([]any, 2)
+       var str string
+       var num int64
+       results[0] = &str
+       results[1] = &num
+       // This MUST decode via msgpack directly (SerializeType dispatch).
+       err := session.Unmarshal(data, results)
+       assert.Nil(t, err)
+
+       assert.Equal(t, str, "msgarg")
+       assert.Equal(t, num, int64(7))
+       assert.Equal(t, session.serializeType, codecNameMsgPack)
+}
+
+func TestServerCodecSession_UnmarshalWrappedRequest_UnknownType(t *testing.T) {
+       t.Parallel()
+
+       session := &tripleServerCodecSession{delegate: &protoBinaryCodec{}}
+       wrapper := &interoperability.TripleRequestWrapper{
+               SerializeType: "unknown",
+               Args:          [][]byte{{0x01}},
+               ArgTypes:      []string{"java.lang.String"},
+       }
+       data, _ := proto.Marshal(wrapper)
+
+       var v any
+       err := session.Unmarshal(data, []any{&v})
+       assert.NotNil(t, err) // explicit error, no silent fallback
+}
+
+func TestServerCodecSession_UnmarshalWrappedRequest_CorruptPayload(t 
*testing.T) {
+       t.Parallel()
+
+       cases := []struct {
+               name          string
+               serializeType string
+               allowed       string // provider allowlist; hessian2 is always 
implicitly allowed
+               payload       []byte
+       }{
+               // 0xff is not a valid hessian2 type code: decode fails.
+               {"hessian2-corrupt", codecNameHessian2, "", []byte{0xff}},
+               // 0xc1 is reserved (never used) in the msgpack spec: decode 
fails.
+               {"msgpack-corrupt", codecNameMsgPack, codecNameMsgPack, 
[]byte{0xc1}},
+       }
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       session := &tripleServerCodecSession{
+                               delegate:             &protoBinaryCodec{},
+                               allowedSerializeType: tc.allowed,
+                       }
+                       wrapper := &interoperability.TripleRequestWrapper{
+                               SerializeType: tc.serializeType,
+                               Args:          [][]byte{tc.payload},
+                               ArgTypes:      []string{"java.lang.String"},
+                       }
+                       data, _ := proto.Marshal(wrapper)
+
+                       var v any
+                       // Payload does not match the declared SerializeType: 
the selected
+                       // inner codec must surface an explicit decode error.
+                       err := session.Unmarshal(data, []any{&v})
+                       assert.NotNil(t, err)
+                       assert.True(t, strings.Contains(err.Error(), "triple 
wrapper request"))
+               })
+       }
+}
+
+func TestProtoWrapperCodec_UnmarshalWrappedResponse_UnknownType(t *testing.T) {
+       t.Parallel()
+
+       codec := newProtoWrapperCodec(&hessian2Codec{})
+       wrapper := &interoperability.TripleResponseWrapper{
+               SerializeType: "unknown",
+               Data:          []byte{0x01},
+       }
+       data, _ := proto.Marshal(wrapper)
+
+       var result any
+       err := codec.Unmarshal(data, &result)
+       assert.NotNil(t, err)
+}
+
+func TestServerCodecSession_MarshalWrappedResponse_Hessian2(t *testing.T) {
+       t.Parallel()
+
+       session := &tripleServerCodecSession{delegate: &protoBinaryCodec{}, 
serializeType: codecNameHessian2}
+       data, err := session.Marshal("hello")
+       assert.Nil(t, err)
+
+       // Decode the produced wrapper and verify round-trip.
+       var wrapper interoperability.TripleResponseWrapper
+       assert.Nil(t, proto.Unmarshal(data, &wrapper))
+       assert.Equal(t, wrapper.SerializeType, codecNameHessian2)
+
+       hessianCodec := &hessian2Codec{}
+       var out any
+       assert.Nil(t, hessianCodec.Unmarshal(wrapper.Data, &out))
+       assert.Equal(t, out, "hello")
+}
+
+func TestServerCodecSession_MarshalWrappedResponse_FollowsSerializeType(t 
*testing.T) {
+       t.Parallel()
+
+       session := &tripleServerCodecSession{delegate: &protoBinaryCodec{}, 
serializeType: codecNameMsgPack}
+       data, err := session.Marshal(int64(42))
+       assert.Nil(t, err)
+
+       var wrapper interoperability.TripleResponseWrapper
+       assert.Nil(t, proto.Unmarshal(data, &wrapper))
+       assert.Equal(t, wrapper.SerializeType, codecNameMsgPack)
+
+       msgp := &msgpackCodec{}
+       var out int64
+       assert.Nil(t, msgp.Unmarshal(wrapper.Data, &out))
+       assert.Equal(t, out, int64(42))
+}
+
+func TestNonIDLResponse_RoundTrip_Hessian2(t *testing.T) {
+       t.Parallel()
+
+       // Server encode.
+       session := &tripleServerCodecSession{delegate: &protoBinaryCodec{}, 
serializeType: codecNameHessian2}
+       data, err := session.Marshal("roundtrip-hessian")
+       assert.Nil(t, err)
+
+       // Client decode (via protoWrapperCodec, simulating a Non-IDL client).
+       codec := newProtoWrapperCodec(&hessian2Codec{})
+       var out any
+       assert.Nil(t, codec.Unmarshal(data, &out))
+       assert.Equal(t, out, "roundtrip-hessian")
+}
+
+func TestNonIDLResponse_RoundTrip_MsgPack(t *testing.T) {
+       t.Parallel()
+
+       session := &tripleServerCodecSession{delegate: &protoBinaryCodec{}, 
serializeType: codecNameMsgPack}
+       data, err := session.Marshal("roundtrip-msgpack")
+       assert.Nil(t, err)
+
+       codec := newProtoWrapperCodec(&msgpackCodec{})
+       var out string
+       assert.Nil(t, codec.Unmarshal(data, &out))
+       assert.Equal(t, out, "roundtrip-msgpack")
+}
+
+func TestServerCodecSession_Allowlist_RejectsUnauthorizedSerializeType(t 
*testing.T) {
+       t.Parallel()
+
+       cases := []struct {
+               name    string
+               allowed string // provider's configured serialization; "" = 
hessian2-only
+       }{
+               {"not-configured-default", ""}, // serialization defaults to 
protobuf (IDL), not msgpack
+               {"configured-hessian2", codecNameHessian2},
+       }
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       session := &tripleServerCodecSession{
+                               delegate:             &protoBinaryCodec{},
+                               allowedSerializeType: tc.allowed,
+                       }
+                       msgpCodec := &msgpackCodec{}
+                       arg, _ := msgpCodec.Marshal("sneaky")
+                       wrapper := &interoperability.TripleRequestWrapper{
+                               SerializeType: codecNameMsgPack,
+                               Args:          [][]byte{arg},
+                               ArgTypes:      []string{"java.lang.String"},
+                       }
+                       data, _ := proto.Marshal(wrapper)
+
+                       var v any
+                       err := session.Unmarshal(data, []any{&v})
+                       assert.NotNil(t, err)
+                       assert.True(t, strings.Contains(err.Error(), "not 
allowed by provider"))
+               })
+       }
+}
+
+func TestServerCodecSession_Allowlist_Hessian2AlwaysAllowed(t *testing.T) {
+       t.Parallel()
+
+       // Provider configured serialization=protobuf (the IDL default); 
hessian2
+       // Non-IDL request must still be accepted.
+       session := &tripleServerCodecSession{
+               delegate:             &protoBinaryCodec{},
+               allowedSerializeType: "protobuf",
+       }
+       hessianCodec := &hessian2Codec{}
+       arg, _ := hessianCodec.Marshal("ok")
+       wrapper := &interoperability.TripleRequestWrapper{
+               SerializeType: codecNameHessian2,
+               Args:          [][]byte{arg},
+               ArgTypes:      []string{"java.lang.String"},
+       }
+       data, _ := proto.Marshal(wrapper)
+
+       var v any
+       err := session.Unmarshal(data, []any{&v})
+       assert.Nil(t, err)
+}
+
+// TestServerCodecSession_Marshal_ProductionShape covers the wire shape 
produced
+// by the real handler path (server.go wrapTripleResponse constructs
+// []any{result}). The session MUST unwrap the one-element container and 
serialize
+// only the scalar return value into TripleResponseWrapper.Data, matching 
Java's
+// ReflectionPackableMethod.WrapResponsePack. Serializing the whole []any would
+// make Hessian2 panic (copySlice on string) and MsgPack return a type error.
+func TestServerCodecSession_Marshal_ProductionShape(t *testing.T) {
+       t.Parallel()
+
+       cases := []struct {
+               name          string
+               serializeType string
+               allowed       string
+               payload       any        // wrapped as []any{payload} to mirror 
production shape
+               newDest       func() any // returns a typed pointer to decode 
into
+               assertDecoded func(t *testing.T, dest any)
+       }{
+               {
+                       name:          "hessian2-string",
+                       serializeType: codecNameHessian2,
+                       payload:       "hello-prod",
+                       newDest:       func() any { var s string; return &s },
+                       assertDecoded: func(t *testing.T, dest any) { 
assert.Equal(t, *(dest.(*string)), "hello-prod") },
+               },
+               {
+                       name:          "hessian2-int64",
+                       serializeType: codecNameHessian2,
+                       payload:       int64(99),
+                       newDest:       func() any { var n int64; return &n },
+                       assertDecoded: func(t *testing.T, dest any) { 
assert.Equal(t, *(dest.(*int64)), int64(99)) },
+               },
+               {
+                       name:          "msgpack-string",
+                       serializeType: codecNameMsgPack,
+                       allowed:       codecNameMsgPack,
+                       payload:       "msgpack-prod",
+                       newDest:       func() any { var s string; return &s },
+                       assertDecoded: func(t *testing.T, dest any) { 
assert.Equal(t, *(dest.(*string)), "msgpack-prod") },
+               },
+       }
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       // Server-side encode with the PRODUCTION shape: 
[]any{result}.
+                       session := &tripleServerCodecSession{
+                               delegate:             &protoBinaryCodec{},
+                               serializeType:        tc.serializeType,
+                               allowedSerializeType: tc.allowed,
+                       }
+                       data, err := session.Marshal([]any{tc.payload})
+                       assert.Nil(t, err)
+
+                       // Inspect the wrapper: Data must hold the scalar, not 
the container.
+                       var wrapper interoperability.TripleResponseWrapper
+                       assert.Nil(t, proto.Unmarshal(data, &wrapper))
+                       assert.Equal(t, wrapper.SerializeType, tc.serializeType)
+                       assert.True(t, len(wrapper.Data) > 0)
+
+                       // Client-side decode via protoWrapperCodec into a 
typed pointer, the
+                       // way a real caller (which knows the return type) 
would.
+                       clientCodec := 
newProtoWrapperCodec(resolveInnerCodecOrFail(t, tc.serializeType))
+                       dest := tc.newDest()
+                       assert.Nil(t, clientCodec.Unmarshal(data, dest))
+                       tc.assertDecoded(t, dest)
+               })
+       }
+}
+
+func resolveInnerCodecOrFail(t *testing.T, serializeType string) Codec {
+       t.Helper()
+       c, err := resolveInnerCodec(serializeType)
+       if err != nil {
+               t.Fatalf("resolveInnerCodec(%q): %v", serializeType, err)
+       }
+       return c
+}
+
+// TestServerCodecSession_Marshal_VoidResponse covers null/void responses: an
+// empty or nil container element must produce an empty Data field (decoded as
+// void by the peer), NOT an attempt to serialize nil.
+func TestServerCodecSession_Marshal_VoidResponse(t *testing.T) {
+       t.Parallel()
+
+       cases := []struct {
+               name string
+               msg  any
+       }{
+               {"empty-slice", []any{}},
+               {"nil-element", []any{nil}},
+       }
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       session := &tripleServerCodecSession{
+                               delegate:      &protoBinaryCodec{},
+                               serializeType: codecNameHessian2,
+                       }
+                       data, err := session.Marshal(tc.msg)
+                       assert.Nil(t, err)
+
+                       var wrapper interoperability.TripleResponseWrapper
+                       assert.Nil(t, proto.Unmarshal(data, &wrapper))
+                       assert.Equal(t, len(wrapper.Data), 0)
+
+                       // Client decodes empty Data as void (no error, no 
value).
+                       clientCodec := newProtoWrapperCodec(&hessian2Codec{})
+                       var got any
+                       assert.Nil(t, clientCodec.Unmarshal(data, &got))
+               })
+       }
+}
+
+// TestProtoWrapperCodec_Unmarshal_EmptyData_UnknownSerializeType covers P1: a
+// corrupt response {SerializeType:"unknown", Data:nil} must be rejected even
+// though Data is empty. Previously it was silently treated as void.
+func TestProtoWrapperCodec_Unmarshal_EmptyData_UnknownSerializeType(t 
*testing.T) {
+       t.Parallel()
+
+       cases := []struct {
+               name          string
+               serializeType string
+               wantErr       bool
+       }{
+               // Unknown/disabled types must error regardless of Data 
emptiness.
+               {"unknown-empty-data", "unknown", true},
+               {"disabled-empty-data", "fastjson", true},
+               // Valid types with empty Data are legitimate void responses.
+               {"hessian2-empty-data", codecNameHessian2, false},
+               {"msgpack-empty-data", codecNameMsgPack, false},
+               // hessian4 aliases hessian2; empty Data is void.
+               {"hessian4-empty-data", "hessian4", false},
+               // Empty SerializeType defaults to hessian2 (backward compat); 
void OK.
+               {"blank-serialize-type", "", false},
+       }
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       codec := newProtoWrapperCodec(&hessian2Codec{})
+                       wrapper := &interoperability.TripleResponseWrapper{
+                               SerializeType: tc.serializeType,
+                               // Data intentionally nil/empty.
+                       }
+                       data, _ := proto.Marshal(wrapper)
+
+                       var got any
+                       err := codec.Unmarshal(data, &got)
+                       if tc.wantErr {
+                               assert.NotNil(t, err)
+                       } else {
+                               assert.Nil(t, err)
+                       }
+               })
+       }
+}
+
+// TestServerCodecSession_Marshal_MultiElementError pins the defined semantics
+// for a multi-element response container: it is a programming error (the
+// production handler always packs exactly one return value), NOT a silent
+// truncation to the first element.
+func TestServerCodecSession_Marshal_MultiElementError(t *testing.T) {
+       t.Parallel()
+
+       session := &tripleServerCodecSession{
+               delegate:      &protoBinaryCodec{},
+               serializeType: codecNameHessian2,
+       }
+       _, err := session.Marshal([]any{"a", "b"})
+       assert.NotNil(t, err)
+}
+
+// TestNonIDLUnary_PublicEntry_EndToEnd drives a non-IDL unary RPC through the
+// public entry points (server: NewUnaryHandler; client: NewClient/CallUnary)
+// over HTTP. The handler returns NewResponse([]any{result}), mirroring the
+// production shape built by server.go wrapTripleResponse, so the session must
+// unwrap the one-element container before serialization. Covers the P0 review
+// matrix: {Hessian2, MsgPack} x {concrete type pointer, *any} destinations.
+func TestNonIDLUnary_PublicEntry_EndToEnd(t *testing.T) {
+       t.Parallel()
+
+       const (
+               service = "/test.NonIDLGreeter"
+               method  = "SayHello"
+       )
+       cases := []struct {
+               name          string
+               clientOption  ClientOption
+               handlerOption HandlerOption // nil for hessian2: always on the 
allowlist
+               newDest       func() any
+       }{
+               {"hessian2-concrete-pointer", WithHessian2(), nil, func() any { 
return new(string) }},
+               {"hessian2-any-pointer", WithHessian2(), nil, func() any { var 
v any; return &v }},
+               {"msgpack-concrete-pointer", WithMsgPack(), 
WithExpectedCodecName(codecNameMsgPack), func() any { return new(string) }},
+               {"msgpack-any-pointer", WithMsgPack(), 
WithExpectedCodecName(codecNameMsgPack), func() any { var v any; return &v }},
+       }
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       t.Parallel()
+
+                       handlerOpts := []HandlerOption{}
+                       if tc.handlerOption != nil {
+                               handlerOpts = append(handlerOpts, 
tc.handlerOption)
+                       }
+                       mux := http.NewServeMux()
+                       mux.Handle(service+"/"+method, NewUnaryHandler(
+                               service+"/"+method,
+                               func() any { return []any{new(string)} },
+                               func(_ context.Context, req *Request) 
(*Response, error) {
+                                       arg := req.Msg.([]any)[0].(*string)
+                                       // Production shape: wrapTripleResponse 
packs []any{result}.
+                                       return NewResponse([]any{"hello:" + 
*arg}), nil
+                               },
+                               handlerOpts...,
+                       ))
+                       server := httptest.NewServer(mux)
+                       t.Cleanup(server.Close)
+
+                       client := NewClient(server.Client(), 
server.URL+service, WithTriple(), tc.clientOption)
+                       resp := &Response{Msg: tc.newDest()}
+                       assert.Nil(t, client.CallUnary(context.Background(), 
NewRequest([]any{"world"}), method, resp))
+
+                       var result string
+                       switch dest := resp.Msg.(type) {
+                       case *string:
+                               result = *dest
+                       case *any:
+                               switch v := (*dest).(type) {
+                               case string:
+                                       result = v
+                               case []byte:
+                                       // ugorji/codec decodes a msgpack str 
into []byte when the
+                                       // destination is *any.
+                                       result = string(v)
+                               }
+                       }
+                       assert.Equal(t, result, "hello:world")
+               })
+       }
+}
diff --git a/protocol/triple/triple_protocol/inner_codec.go 
b/protocol/triple/triple_protocol/inner_codec.go
new file mode 100644
index 000000000..26a9c17fa
--- /dev/null
+++ b/protocol/triple/triple_protocol/inner_codec.go
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package triple_protocol
+
+import (
+       "fmt"
+       "sort"
+)
+
+// innerCodecRegistry is the allowed-set of Triple non-IDL inner serialization
+// codecs. The name key MUST match the SerializeType string the client writes
+// into TripleRequestWrapper.SerializeType / 
TripleResponseWrapper.SerializeType
+// (e.g. "hessian2", "msgpack"). An absent entry is a disabled serialization.
+type innerCodecRegistry struct {
+       items map[string]Codec
+}
+
+var innerCodecs = &innerCodecRegistry{items: make(map[string]Codec)}
+
+func init() {
+       registerInnerCodec(codecNameHessian2, &hessian2Codec{})
+       registerInnerCodec(codecNameMsgPack, &msgpackCodec{})
+}
+
+// registerInnerCodec registers an inner codec under the given name.
+func registerInnerCodec(name string, c Codec) {
+       if c == nil {
+               panic(fmt.Sprintf("triple_protocol: registerInnerCodec(%q): nil 
codec", name))
+       }
+       if c.Name() != name {
+               panic(fmt.Sprintf("triple_protocol: registerInnerCodec(%q): 
codec Name() = %q, must match the registered name",
+                       name, c.Name()))
+       }
+       innerCodecs.items[name] = c
+}
+
+// getInnerCodec looks up a registered inner codec by name.
+// Returns (nil, false) when the name is unknown or disabled (unregistered).
+func getInnerCodec(name string) (Codec, bool) {
+       c, ok := innerCodecs.items[name]
+       return c, ok
+}
+
+// innerCodecNames returns the registered inner codec names, sorted for stable
+// error diagnostics (map iteration order is unspecified).
+func innerCodecNames() []string {
+       names := make([]string, 0, len(innerCodecs.items))
+       for n := range innerCodecs.items {
+               names = append(names, n)
+       }
+       sort.Strings(names)
+       return names
+}
+
+// resolveInnerCodec looks up the inner codec registered under serializeType
+// in the inner codec registry. An empty serializeType defaults to hessian2 for
+// backward compatibility.
+//
+// Dubbo Java writes "hessian4" into the wrapper (TripleConstants.HESSIAN4)
+// while its on-wire encoding is Hessian2-compatible; the Java receiver maps it
+// back to "hessian2" in ReflectionPackableMethod.convertHessianFromWrapper. Go
+// mirrors that single alias so a Java non-IDL client is not rejected.
+func resolveInnerCodec(serializeType string) (Codec, error) {
+       if serializeType == "" || serializeType == "hessian4" {
+               serializeType = codecNameHessian2
+       }
+       c, ok := getInnerCodec(serializeType)
+       if !ok {
+               return nil, fmt.Errorf("unsupported or disabled serialize type 
%q (registered: %v)",
+                       serializeType, innerCodecNames())
+       }
+       return c, nil
+}
diff --git a/protocol/triple/triple_protocol/protocol_triple.go 
b/protocol/triple/triple_protocol/protocol_triple.go
index 5d322730b..04de63618 100644
--- a/protocol/triple/triple_protocol/protocol_triple.go
+++ b/protocol/triple/triple_protocol/protocol_triple.go
@@ -34,8 +34,6 @@ import (
 )
 
 import (
-       "github.com/dubbogo/gost/log/logger"
-
        "google.golang.org/protobuf/types/known/anypb"
 )
 
@@ -162,7 +160,6 @@ func (h *tripleHandler) NewConn(
                contentType,
        )
        codec := h.Codecs.Get(codecName)
-       backupCodec := h.Codecs.Get(h.FallbackCodecName)
        // todo:// need to figure it out
        // The codec can be nil in the GET request case; that's okay: when 
failed
        // is non-nil, codec is never used.
@@ -187,15 +184,21 @@ func (h *tripleHandler) NewConn(
                Addr:     request.RemoteAddr,
                Protocol: ProtocolTriple,
        }
-       conn = &tripleUnaryHandlerConn{
+       var codecSession = codec
+       if codec != nil && getWireCodecName(codec) == codecNameProto {
+               codecSession = &tripleServerCodecSession{
+                       delegate:             codec,
+                       allowedSerializeType: h.FallbackCodecName,
+               }
+       }
+       hc := &tripleUnaryHandlerConn{
                spec:           h.Spec,
                peer:           peer,
                request:        request,
                responseWriter: responseWriter,
                marshaler: tripleUnaryMarshaler{
                        writer:           responseWriter,
-                       codec:            codec,
-                       backupCodec:      backupCodec,
+                       codec:            codecSession,
                        compressMinBytes: h.CompressMinBytes,
                        compressionName:  responseCompression,
                        compressionPool:  
h.CompressionPools.Get(responseCompression),
@@ -205,15 +208,14 @@ func (h *tripleHandler) NewConn(
                },
                unmarshaler: tripleUnaryUnmarshaler{
                        reader:          requestBody,
-                       codec:           codec,
-                       backupCodec:     backupCodec,
+                       codec:           codecSession,
                        compressionPool: 
h.CompressionPools.Get(requestCompression),
                        bufferPool:      h.BufferPool,
                        readMaxBytes:    h.ReadMaxBytes,
                },
                responseTrailer: make(http.Header),
        }
-       conn = wrapHandlerConnWithCodedErrors(conn)
+       conn = wrapHandlerConnWithCodedErrors(hc)
 
        if failed != nil {
                // Negotiation failed, so we can't establish a stream.
@@ -484,7 +486,6 @@ func (hc *tripleUnaryHandlerConn) writeResponseHeader(err 
error) {
 type tripleUnaryMarshaler struct {
        writer           io.Writer
        codec            Codec
-       backupCodec      Codec // backupCodec is the fallback codec when 
primary codec fails
        compressMinBytes int
        compressionName  string
        compressionPool  *compressionPool
@@ -499,13 +500,7 @@ func (m *tripleUnaryMarshaler) Marshal(message any) *Error 
{
        }
        data, err := m.codec.Marshal(message)
        if err != nil {
-               if m.backupCodec != nil && m.codec.Name() != 
m.backupCodec.Name() {
-                       logger.Warnf("[Triple] failed to marshal message with 
primary codec %s, trying fallback codec %s", m.codec.Name(), 
m.backupCodec.Name())
-                       data, err = m.backupCodec.Marshal(message)
-               }
-               if err != nil {
-                       return errorf(CodeInternal, "marshal message: %w", err)
-               }
+               return errorf(CodeInternal, "marshal message: %w", err)
        }
        // Can't avoid allocating the slice, but we can reuse it.
        uncompressed := bytes.NewBuffer(data)
@@ -549,7 +544,6 @@ func (m *tripleUnaryRequestMarshaler) Marshal(message any) 
*Error {
 type tripleUnaryUnmarshaler struct {
        reader          io.Reader
        codec           Codec
-       backupCodec     Codec // backupCodec is the fallback codec when primary 
codec fails
        compressionPool *compressionPool
        bufferPool      *bufferPool
        alreadyRead     bool
@@ -557,14 +551,7 @@ type tripleUnaryUnmarshaler struct {
 }
 
 func (u *tripleUnaryUnmarshaler) Unmarshal(message any) *Error {
-       err := u.UnmarshalFunc(message, u.codec.Unmarshal)
-       if err != nil {
-               if u.backupCodec != nil && u.codec.Name() != 
u.backupCodec.Name() {
-                       logger.Warnf("[Triple] failed to unmarshal message with 
primary codec %s, trying fallback codec %s", u.codec.Name(), 
u.backupCodec.Name())
-                       err = u.UnmarshalFunc(message, u.backupCodec.Unmarshal)
-               }
-       }
-       return err
+       return u.UnmarshalFunc(message, u.codec.Unmarshal)
 }
 
 func (u *tripleUnaryUnmarshaler) UnmarshalFunc(message any, unmarshal 
func([]byte, any) error) *Error {

Reply via email to