From 0fe7af906515b9cd74a826aa61e16a25af25cfa1 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 8 Jun 2026 17:38:09 +0800 Subject: [PATCH 1/2] refactor(hessian2): use generic exception from hessian2 --- protocol/dubbo/hessian2/hessian_response.go | 55 +++--- .../dubbo/hessian2/hessian_response_test.go | 177 ++++++++++++++++++ protocol/dubbo/impl/hessian.go | 8 +- protocol/dubbo/impl/hessian_test.go | 40 +++- 4 files changed, 240 insertions(+), 40 deletions(-) diff --git a/protocol/dubbo/hessian2/hessian_response.go b/protocol/dubbo/hessian2/hessian_response.go index b3dc37f8e8..06e18012ae 100644 --- a/protocol/dubbo/hessian2/hessian_response.go +++ b/protocol/dubbo/hessian2/hessian_response.go @@ -41,49 +41,40 @@ type DubboResponse struct { Attachments map[string]any } -// GenericException keeps Java exception class and message. -type GenericException struct { - ExceptionClass string - ExceptionMessage string -} - -// Error returns a readable error string. -func (e GenericException) Error() string { - if e.ExceptionClass == "" { - return e.ExceptionMessage - } - if e.ExceptionMessage == "" { - return e.ExceptionClass - } - return "java exception: " + e.ExceptionClass + " - " + e.ExceptionMessage -} - -// ToGenericException converts decoded exception to GenericException when possible. -func ToGenericException(expt any) (*GenericException, bool) { +// ToGenericException converts decoded exception to DubboGenericException when possible. +func ToGenericException(expt any) (*java_exception.DubboGenericException, bool) { switch v := expt.(type) { - case *GenericException: - return v, true - case GenericException: - return &v, true case *java_exception.DubboGenericException: - return &GenericException{ExceptionClass: v.ExceptionClass, ExceptionMessage: v.ExceptionMessage}, true + return v, true case java_exception.DubboGenericException: - return &GenericException{ExceptionClass: v.ExceptionClass, ExceptionMessage: v.ExceptionMessage}, true + return &v, true case java_exception.Throwabler: - return &GenericException{ExceptionClass: v.JavaClassName(), ExceptionMessage: v.Error()}, true + return newGenericException(v.JavaClassName(), v.Error()), true case string: return parseLegacyException(v), true } return nil, false } -func parseLegacyException(exStr string) *GenericException { +func parseLegacyException(exStr string) *java_exception.DubboGenericException { const prefix = "java exception:" msg := strings.TrimSpace(exStr) if strings.HasPrefix(msg, prefix) { msg = strings.TrimSpace(strings.TrimPrefix(msg, prefix)) } - return &GenericException{ExceptionClass: "java.lang.Exception", ExceptionMessage: msg} + return newGenericException("java.lang.Exception", msg) +} + +func newGenericException(exceptionClass, exceptionMessage string) *java_exception.DubboGenericException { + exception := java_exception.NewDubboGenericException(exceptionClass, exceptionMessage) + if exceptionClass == "" { + exception.DetailMessage = exceptionMessage + } else if exceptionMessage == "" { + exception.DetailMessage = exceptionClass + } else { + exception.DetailMessage = "java exception: " + exceptionClass + " - " + exceptionMessage + } + return exception } // NewResponse create a new DubboResponse @@ -163,10 +154,10 @@ func packResponse(header DubboHeader, ret any) ([]byte, error) { return nil, perrors.Errorf("encoding response failed: %v", err) } switch ex := response.Exception.(type) { - case *GenericException: - err = encoder.Encode(java_exception.NewDubboGenericException(ex.ExceptionClass, ex.ExceptionMessage)) - case GenericException: - err = encoder.Encode(java_exception.NewDubboGenericException(ex.ExceptionClass, ex.ExceptionMessage)) + case *java_exception.DubboGenericException: + err = encoder.Encode(ex) + case java_exception.DubboGenericException: + err = encoder.Encode(ex) case java_exception.Throwabler: err = encoder.Encode(ex) default: diff --git a/protocol/dubbo/hessian2/hessian_response_test.go b/protocol/dubbo/hessian2/hessian_response_test.go index 17ce786472..7564e4defa 100644 --- a/protocol/dubbo/hessian2/hessian_response_test.go +++ b/protocol/dubbo/hessian2/hessian_response_test.go @@ -18,6 +18,8 @@ package hessian2 import ( + "bufio" + "bytes" "reflect" "sync" "testing" @@ -25,6 +27,7 @@ import ( import ( hessian "github.com/apache/dubbo-go-hessian2" + "github.com/apache/dubbo-go-hessian2/java_exception" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -205,6 +208,180 @@ func TestIsSupportResponseAttachmentConcurrent(t *testing.T) { wg.Wait() } +func TestToGenericExceptionUsesHessianExceptionType(t *testing.T) { + exception, ok := ToGenericException(java_exception.DubboGenericException{ + ExceptionClass: "com.example.UserNotFoundException", + ExceptionMessage: "user not found", + }) + + require.True(t, ok) + require.IsType(t, &java_exception.DubboGenericException{}, exception) + assert.Equal(t, "com.example.UserNotFoundException", exception.ExceptionClass) + assert.Equal(t, "user not found", exception.ExceptionMessage) +} + +func TestToGenericExceptionConversions(t *testing.T) { + pointerException := java_exception.NewDubboGenericException("com.example.PointerException", "pointer message") + + tests := []struct { + name string + input any + wantOK bool + wantClass string + wantMessage string + wantDetailString string + }{ + { + name: "generic exception pointer", + input: pointerException, + wantOK: true, + wantClass: "com.example.PointerException", + wantMessage: "pointer message", + }, + { + name: "throwable", + input: java_exception.NewThrowable("throwable message"), + wantOK: true, + wantClass: "java.lang.Throwable", + wantMessage: "throwable message", + wantDetailString: "java exception: java.lang.Throwable - throwable message", + }, + { + name: "legacy exception string", + input: "java exception: user not found", + wantOK: true, + wantClass: "java.lang.Exception", + wantMessage: "user not found", + wantDetailString: "java exception: java.lang.Exception - user not found", + }, + { + name: "plain string", + input: "plain failure", + wantOK: true, + wantClass: "java.lang.Exception", + wantMessage: "plain failure", + wantDetailString: "java exception: java.lang.Exception - plain failure", + }, + { + name: "unsupported type", + input: 42, + wantOK: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + exception, ok := ToGenericException(test.input) + assert.Equal(t, test.wantOK, ok) + if !test.wantOK { + assert.Nil(t, exception) + return + } + + require.NotNil(t, exception) + assert.Equal(t, test.wantClass, exception.ExceptionClass) + assert.Equal(t, test.wantMessage, exception.ExceptionMessage) + assert.Equal(t, test.wantDetailString, exception.Error()) + }) + } +} + +func TestNewGenericExceptionDetailMessage(t *testing.T) { + tests := []struct { + name string + class string + message string + wantDetails string + }{ + { + name: "message only", + message: "message only", + wantDetails: "message only", + }, + { + name: "class only", + class: "com.example.EmptyMessage", + wantDetails: "com.example.EmptyMessage", + }, + { + name: "class and message", + class: "com.example.UserNotFoundException", + message: "user not found", + wantDetails: "java exception: com.example.UserNotFoundException - user not found", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + exception := newGenericException(test.class, test.message) + assert.Equal(t, test.class, exception.ExceptionClass) + assert.Equal(t, test.message, exception.ExceptionMessage) + assert.Equal(t, test.wantDetails, exception.Error()) + }) + } +} + +func TestPackResponseWithGenericExceptionPointer(t *testing.T) { + tests := []struct { + name string + exception error + wantClass string + wantMessage string + }{ + { + name: "generic exception pointer", + exception: java_exception.NewDubboGenericException( + "com.example.UserNotFoundException", + "user not found", + ), + wantClass: "com.example.UserNotFoundException", + wantMessage: "user not found", + }, + { + name: "generic exception value", + exception: java_exception.DubboGenericException{ + ExceptionClass: "com.example.IllegalStateException", + ExceptionMessage: "illegal state", + }, + wantClass: "com.example.IllegalStateException", + wantMessage: "illegal state", + }, + { + name: "throwable", + exception: java_exception.NewThrowable("throwable message"), + wantClass: "java.lang.Throwable", + wantMessage: "throwable message", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + body := NewResponse(nil, test.exception, map[string]any{}) + data, err := packResponse(DubboHeader{ + SerialID: 2, + Type: PackageResponse, + ID: 1, + ResponseStatus: Response_OK, + }, body) + require.NoError(t, err) + + codecR := NewHessianCodec(bufio.NewReader(bytes.NewReader(data))) + header := &DubboHeader{} + require.NoError(t, codecR.ReadHeader(header)) + assert.Equal(t, PackageResponse, header.Type&PackageResponse) + assert.Equal(t, Response_OK, header.ResponseStatus) + + decodedResponse := &DubboResponse{} + require.NoError(t, codecR.ReadBody(decodedResponse)) + + ge, ok := decodedResponse.Exception.(*java_exception.DubboGenericException) + require.True(t, ok) + assert.Equal(t, test.wantClass, ge.ExceptionClass) + assert.Equal(t, test.wantMessage, ge.ExceptionMessage) + }) + } +} + func TestVersion2Int(t *testing.T) { v := version2Int("2.1.3") assert.Equal(t, 2010300, v) diff --git a/protocol/dubbo/impl/hessian.go b/protocol/dubbo/impl/hessian.go index 76fae02969..5ab2d3e847 100644 --- a/protocol/dubbo/impl/hessian.go +++ b/protocol/dubbo/impl/hessian.go @@ -87,10 +87,10 @@ func marshalResponse(encoder *hessian.Encoder, p DubboPackage) ([]byte, error) { if response.Exception != nil { // throw error _ = encoder.Encode(resWithException) switch ex := response.Exception.(type) { - case *hessian2.GenericException: - _ = encoder.Encode(java_exception.NewDubboGenericException(ex.ExceptionClass, ex.ExceptionMessage)) - case hessian2.GenericException: - _ = encoder.Encode(java_exception.NewDubboGenericException(ex.ExceptionClass, ex.ExceptionMessage)) + case *java_exception.DubboGenericException: + _ = encoder.Encode(ex) + case java_exception.DubboGenericException: + _ = encoder.Encode(ex) case java_exception.Throwabler: _ = encoder.Encode(ex) default: diff --git a/protocol/dubbo/impl/hessian_test.go b/protocol/dubbo/impl/hessian_test.go index b62c8a1114..5cfb91b494 100644 --- a/protocol/dubbo/impl/hessian_test.go +++ b/protocol/dubbo/impl/hessian_test.go @@ -34,7 +34,6 @@ import ( import ( "dubbo.apache.org/dubbo-go/v3/common" - "dubbo.apache.org/dubbo-go/v3/protocol/dubbo/hessian2" ) const ( @@ -512,7 +511,7 @@ func TestMarshalResponse(t *testing.T) { ResponseStatus: Response_OK, }, Body: &ResponsePayload{ - Exception: hessian2.GenericException{ + Exception: java_exception.DubboGenericException{ ExceptionClass: "com.example.UserNotFoundException", ExceptionMessage: "user not found", }, @@ -543,6 +542,39 @@ func TestMarshalResponse(t *testing.T) { } }) + t.Run("response with generic exception pointer", func(t *testing.T) { + encoder := hessian.NewEncoder() + pkg := DubboPackage{ + Header: DubboHeader{ + Type: PackageResponse, + ResponseStatus: Response_OK, + }, + Body: &ResponsePayload{ + Exception: java_exception.NewDubboGenericException( + "com.example.UserNotFoundException", + "user not found", + ), + Attachments: map[string]any{}, + }, + } + + data, err := marshalResponse(encoder, pkg) + require.NoError(t, err) + assert.NotNil(t, data) + + decoder := hessian.NewDecoder(data) + rspType, err := decoder.Decode() + require.NoError(t, err) + assert.EqualValues(t, RESPONSE_WITH_EXCEPTION, rspType) + + expt, err := decoder.Decode() + require.NoError(t, err) + ge, ok := expt.(*java_exception.DubboGenericException) + require.True(t, ok) + assert.Equal(t, "com.example.UserNotFoundException", ge.ExceptionClass) + assert.Equal(t, "user not found", ge.ExceptionMessage) + }) + t.Run("response with throwable exception", func(t *testing.T) { encoder := hessian.NewEncoder() pkg := DubboPackage{ @@ -721,7 +753,7 @@ func TestUnmarshalResponseBody(t *testing.T) { require.NoError(t, err) response := EnsureResponsePayload(pkg.Body) - ge, ok := response.Exception.(*hessian2.GenericException) + ge, ok := response.Exception.(*java_exception.DubboGenericException) require.True(t, ok) assert.Equal(t, "com.example.UserNotFoundException", ge.ExceptionClass) assert.Equal(t, "user not found", ge.ExceptionMessage) @@ -787,7 +819,7 @@ func TestUnmarshalResponseBody(t *testing.T) { require.NoError(t, err) response := EnsureResponsePayload(pkg.Body) - ge, ok := response.Exception.(*hessian2.GenericException) + ge, ok := response.Exception.(*java_exception.DubboGenericException) require.True(t, ok) assert.Equal(t, "java.lang.Exception", ge.ExceptionClass) assert.Equal(t, "user not found", ge.ExceptionMessage) From 74528bba7672871e811d9ac44faba4f3e9aea8a5 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Wed, 10 Jun 2026 09:32:37 +0800 Subject: [PATCH 2/2] test(hessian2): skip empty generic exception detail assertion --- protocol/dubbo/hessian2/hessian_response_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/protocol/dubbo/hessian2/hessian_response_test.go b/protocol/dubbo/hessian2/hessian_response_test.go index 7564e4defa..d9662a2cdc 100644 --- a/protocol/dubbo/hessian2/hessian_response_test.go +++ b/protocol/dubbo/hessian2/hessian_response_test.go @@ -281,7 +281,9 @@ func TestToGenericExceptionConversions(t *testing.T) { require.NotNil(t, exception) assert.Equal(t, test.wantClass, exception.ExceptionClass) assert.Equal(t, test.wantMessage, exception.ExceptionMessage) - assert.Equal(t, test.wantDetailString, exception.Error()) + if test.wantDetailString != "" { + assert.Equal(t, test.wantDetailString, exception.Error()) + } }) } }