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

alexstocks 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 157c9d875 feat(generic): add exception class handle (#3183)
157c9d875 is described below

commit 157c9d875ba7d1c2c8d09d684c3bc3bc9e67f16a
Author: 翎 <[email protected]>
AuthorDate: Sat Jan 31 19:06:01 2026 +0800

    feat(generic): add exception class handle (#3183)
    
    * feat(generic): add GenericException with ExceptionClass
---
 cluster/cluster/failback/cluster_test.go      | 17 ++++---
 protocol/dubbo/hessian2/hessian_dubbo.go      | 12 +++--
 protocol/dubbo/hessian2/hessian_dubbo_test.go |  2 +-
 protocol/dubbo/hessian2/hessian_response.go   | 60 ++++++++++++++++++++--
 protocol/dubbo/impl/hessian.go                | 20 +++++---
 protocol/dubbo/impl/hessian_test.go           | 72 +++++++++++++++++++++++++++
 protocol/triple/client_external_test.go       |  4 +-
 7 files changed, 163 insertions(+), 24 deletions(-)

diff --git a/cluster/cluster/failback/cluster_test.go 
b/cluster/cluster/failback/cluster_test.go
index 872eed76f..60aa08cd2 100644
--- a/cluster/cluster/failback/cluster_test.go
+++ b/cluster/cluster/failback/cluster_test.go
@@ -131,7 +131,7 @@ func TestFailbackRetryOneSuccess(t *testing.T) {
        assert.Equal(t, int64(0), clusterInvoker.taskList.Len())
 }
 
-// failed firstly, and failed again after ech retry time.
+// failed firstly, and failed again after ech retry time
 func TestFailbackRetryFailed(t *testing.T) {
        ctrl := gomock.NewController(t)
        defer ctrl.Finish()
@@ -145,19 +145,19 @@ func TestFailbackRetryFailed(t *testing.T) {
        mockFailedResult := &result.RPCResult{Err: perrors.New("error")}
        invoker.EXPECT().Invoke(gomock.Any(), 
gomock.Any()).Return(mockFailedResult)
 
-       //
        var wg sync.WaitGroup
+       var retryCount atomic.Int64
        retries := 2
        wg.Add(retries)
 
        // add retry call that eventually failed.
-       for i := 0; i < retries; i++ {
-               invoker.EXPECT().Invoke(gomock.Any(), 
gomock.Any()).DoAndReturn(func(context.Context, base.Invocation) result.Result {
-                       // with exponential backoff, retries happen with 
increasing intervals starting from ~1s
+       invoker.EXPECT().Invoke(gomock.Any(), 
gomock.Any()).DoAndReturn(func(context.Context, base.Invocation) result.Result {
+               // with exponential backoff, retries happen with increasing 
intervals starting from ~1s
+               if retryCount.Add(1) <= int64(retries) {
                        wg.Done()
-                       return mockFailedResult
-               })
-       }
+               }
+               return mockFailedResult
+       }).MinTimes(retries)
 
        // first call should failed.
        result := clusterInvoker.Invoke(context.Background(), 
&invocation.RPCInvocation{})
@@ -184,6 +184,7 @@ func TestFailbackRetryFailed10Times(t *testing.T) {
        invoker := mock.NewMockInvoker(ctrl)
        clusterInvoker := registerFailback(invoker).(*failbackClusterInvoker)
        clusterInvoker.maxRetries = 10
+       clusterInvoker.failbackTasks = 20
 
        invoker.EXPECT().IsAvailable().Return(true).AnyTimes()
        invoker.EXPECT().GetURL().Return(failbackUrl).AnyTimes()
diff --git a/protocol/dubbo/hessian2/hessian_dubbo.go 
b/protocol/dubbo/hessian2/hessian_dubbo.go
index 23366f1b0..cba86ce88 100644
--- a/protocol/dubbo/hessian2/hessian_dubbo.go
+++ b/protocol/dubbo/hessian2/hessian_dubbo.go
@@ -151,7 +151,7 @@ func (h *HessianCodec) ReadHeader(header *DubboHeader) 
error {
                }
        }
 
-       //// read header
+       // read header
 
        if buf[0] != MAGIC_HIGH && buf[1] != MAGIC_LOW {
                return ErrIllegalPackage
@@ -240,9 +240,15 @@ func (h *HessianCodec) ReadBody(rspObj any) error {
                }
                rsp, ok := rspObj.(*DubboResponse)
                if !ok {
-                       return perrors.Errorf("java exception:%s", 
exception.(string))
+                       return perrors.Errorf("java exception: %v", exception)
+               }
+               if g, ok := ToGenericException(exception); ok {
+                       rsp.Exception = g
+               } else if e, ok := exception.(error); ok {
+                       rsp.Exception = e
+               } else {
+                       rsp.Exception = perrors.Errorf("java exception: %v", 
exception)
                }
-               rsp.Exception = perrors.Errorf("java exception:%s", 
exception.(string))
                return nil
        case PackageRequest | PackageHeartbeat, PackageResponse | 
PackageHeartbeat:
        case PackageRequest:
diff --git a/protocol/dubbo/hessian2/hessian_dubbo_test.go 
b/protocol/dubbo/hessian2/hessian_dubbo_test.go
index 3514d78c6..55177caec 100644
--- a/protocol/dubbo/hessian2/hessian_dubbo_test.go
+++ b/protocol/dubbo/hessian2/hessian_dubbo_test.go
@@ -151,7 +151,7 @@ func TestResponse(t *testing.T) {
        errorMsg := "error!!!!!"
        decodedResponse.RspObj = nil
        doTestResponse(t, PackageResponse, Response_SERVER_ERROR, errorMsg, 
decodedResponse, func() {
-               assert.Equal(t, "java exception:error!!!!!", 
decodedResponse.Exception.Error())
+               assert.Equal(t, "java exception: java.lang.Exception - 
error!!!!!", decodedResponse.Exception.Error())
        })
 
        decodedResponse.RspObj = nil
diff --git a/protocol/dubbo/hessian2/hessian_response.go 
b/protocol/dubbo/hessian2/hessian_response.go
index a20ec30ec..8a2c40c29 100644
--- a/protocol/dubbo/hessian2/hessian_response.go
+++ b/protocol/dubbo/hessian2/hessian_response.go
@@ -41,6 +41,51 @@ 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) {
+       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
+       case java_exception.DubboGenericException:
+               return &GenericException{ExceptionClass: v.ExceptionClass, 
ExceptionMessage: v.ExceptionMessage}, true
+       case java_exception.Throwabler:
+               return &GenericException{ExceptionClass: v.JavaClassName(), 
ExceptionMessage: v.Error()}, true
+       case string:
+               return parseLegacyException(v), true
+       }
+       return nil, false
+}
+
+func parseLegacyException(exStr string) *GenericException {
+       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}
+}
+
 // NewResponse create a new DubboResponse
 func NewResponse(rspObj any, exception error, attachments map[string]any) 
*DubboResponse {
        if attachments == nil {
@@ -117,9 +162,14 @@ func packResponse(header DubboHeader, ret any) ([]byte, 
error) {
                                if err != nil {
                                        return nil, perrors.Errorf("encoding 
response failed: %v", err)
                                }
-                               if t, ok := 
response.Exception.(java_exception.Throwabler); ok {
-                                       err = encoder.Encode(t)
-                               } else {
+                               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.Throwabler:
+                                       err = encoder.Encode(ex)
+                               default:
                                        err = 
encoder.Encode(java_exception.NewThrowable(response.Exception.Error()))
                                }
                                if err != nil {
@@ -203,7 +253,9 @@ func unpackResponseBody(decoder *hessian.Decoder, resp any) 
error {
                        }
                }
 
-               if e, ok := expt.(error); ok {
+               if g, ok := ToGenericException(expt); ok {
+                       response.Exception = g
+               } else if e, ok := expt.(error); ok {
                        response.Exception = e
                } else {
                        response.Exception = perrors.Errorf("got exception: 
%+v", expt)
diff --git a/protocol/dubbo/impl/hessian.go b/protocol/dubbo/impl/hessian.go
index c9c7e82f0..c639fabc6 100644
--- a/protocol/dubbo/impl/hessian.go
+++ b/protocol/dubbo/impl/hessian.go
@@ -37,6 +37,7 @@ import (
 import (
        "dubbo.apache.org/dubbo-go/v3/common"
        "dubbo.apache.org/dubbo-go/v3/common/constant"
+       "dubbo.apache.org/dubbo-go/v3/protocol/dubbo/hessian2"
 )
 
 type HessianSerializer struct{}
@@ -85,9 +86,14 @@ func marshalResponse(encoder *hessian.Encoder, p 
DubboPackage) ([]byte, error) {
 
                        if response.Exception != nil { // throw error
                                _ = encoder.Encode(resWithException)
-                               if t, ok := 
response.Exception.(java_exception.Throwabler); ok {
-                                       _ = encoder.Encode(t)
-                               } else {
+                               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.Throwabler:
+                                       _ = encoder.Encode(ex)
+                               default:
                                        _ = 
encoder.Encode(java_exception.NewThrowable(response.Exception.Error()))
                                }
                        } else {
@@ -300,7 +306,9 @@ func unmarshalResponseBody(body []byte, p *DubboPackage) 
error {
                        }
                }
 
-               if e, ok := expt.(error); ok {
+               if g, ok := hessian2.ToGenericException(expt); ok {
+                       response.Exception = g
+               } else if e, ok := expt.(error); ok {
                        response.Exception = e
                } else {
                        response.Exception = perrors.Errorf("got exception: 
%+v", expt)
@@ -425,9 +433,7 @@ func getArgType(v any) string {
        }
 
        switch v := v.(type) {
-       // Serialized tags for base types
-       case nil:
-               return "V"
+       // Serialized tags for base types=
        case bool:
                return "Z"
        case []bool:
diff --git a/protocol/dubbo/impl/hessian_test.go 
b/protocol/dubbo/impl/hessian_test.go
index 494683318..100552a6d 100644
--- a/protocol/dubbo/impl/hessian_test.go
+++ b/protocol/dubbo/impl/hessian_test.go
@@ -33,6 +33,7 @@ import (
 
 import (
        "dubbo.apache.org/dubbo-go/v3/common"
+       "dubbo.apache.org/dubbo-go/v3/protocol/dubbo/hessian2"
 )
 
 const (
@@ -485,6 +486,45 @@ func TestMarshalResponse(t *testing.T) {
                assert.NotNil(t, data)
        })
 
+       t.Run("response with generic exception", func(t *testing.T) {
+               encoder := hessian.NewEncoder()
+               pkg := DubboPackage{
+                       Header: DubboHeader{
+                               Type:           PackageResponse,
+                               ResponseStatus: Response_OK,
+                       },
+                       Body: &ResponsePayload{
+                               Exception: hessian2.GenericException{
+                                       ExceptionClass:   
"com.example.UserNotFoundException",
+                                       ExceptionMessage: "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)
+               switch ge := expt.(type) {
+               case *java_exception.DubboGenericException:
+                       assert.Equal(t, "com.example.UserNotFoundException", 
ge.ExceptionClass)
+                       assert.Equal(t, "user not found", ge.ExceptionMessage)
+               case java_exception.DubboGenericException:
+                       assert.Equal(t, "com.example.UserNotFoundException", 
ge.ExceptionClass)
+                       assert.Equal(t, "user not found", ge.ExceptionMessage)
+               default:
+                       require.Failf(t, "unexpected exception type", "%T", 
expt)
+               }
+       })
+
        t.Run("response with throwable exception", func(t *testing.T) {
                encoder := hessian.NewEncoder()
                pkg := DubboPackage{
@@ -653,6 +693,22 @@ func TestUnmarshalResponseBody(t *testing.T) {
                assert.Error(t, response.Exception)
        })
 
+       t.Run("response with generic exception", func(t *testing.T) {
+               encoder := hessian.NewEncoder()
+               _ = encoder.Encode(RESPONSE_WITH_EXCEPTION)
+               _ = 
encoder.Encode(java_exception.NewDubboGenericException("com.example.UserNotFoundException",
 "user not found"))
+
+               pkg := &DubboPackage{}
+               err := unmarshalResponseBody(encoder.Buffer(), pkg)
+               require.NoError(t, err)
+
+               response := EnsureResponsePayload(pkg.Body)
+               ge, ok := response.Exception.(*hessian2.GenericException)
+               require.True(t, ok)
+               assert.Equal(t, "com.example.UserNotFoundException", 
ge.ExceptionClass)
+               assert.Equal(t, "user not found", ge.ExceptionMessage)
+       })
+
        t.Run("response with exception and attachments", func(t *testing.T) {
                encoder := hessian.NewEncoder()
                _ = encoder.Encode(RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS)
@@ -703,6 +759,22 @@ func TestUnmarshalResponseBody(t *testing.T) {
                assert.Error(t, response.Exception)
        })
 
+       t.Run("response with legacy exception string", func(t *testing.T) {
+               encoder := hessian.NewEncoder()
+               _ = encoder.Encode(RESPONSE_WITH_EXCEPTION)
+               _ = encoder.Encode("java exception: user not found")
+
+               pkg := &DubboPackage{}
+               err := unmarshalResponseBody(encoder.Buffer(), pkg)
+               require.NoError(t, err)
+
+               response := EnsureResponsePayload(pkg.Body)
+               ge, ok := response.Exception.(*hessian2.GenericException)
+               require.True(t, ok)
+               assert.Equal(t, "java.lang.Exception", ge.ExceptionClass)
+               assert.Equal(t, "user not found", ge.ExceptionMessage)
+       })
+
        t.Run("response with invalid attachments", func(t *testing.T) {
                encoder := hessian.NewEncoder()
                _ = encoder.Encode(RESPONSE_VALUE_WITH_ATTACHMENTS)
diff --git a/protocol/triple/client_external_test.go 
b/protocol/triple/client_external_test.go
index 049b3b041..bfbd4d641 100644
--- a/protocol/triple/client_external_test.go
+++ b/protocol/triple/client_external_test.go
@@ -179,7 +179,9 @@ func testClientInvokeWithTimeout(t *testing.T, tlsConfig 
*global.TLSConfig) {
        t.Logf("invoke result err: %+v, duration: %v", err, duration)
 
        require.Error(t, err)
-       assert.Truef(t, errors.Is(err, context.DeadlineExceeded), "context 
deadline exceeded")
+       var netErr interface{ Timeout() bool }
+       isTimeout := errors.Is(err, context.DeadlineExceeded) || 
(errors.As(err, &netErr) && netErr.Timeout())
+       assert.Truef(t, isTimeout, "expected timeout error, got: %v", err)
 
        // 7. Ensure that the duration is at least the timeout duration
        assert.GreaterOrEqual(t, duration, timeout)

Reply via email to