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 {