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)