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

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


The following commit(s) were added to refs/heads/master by this push:
     new 28ed891  Add: GenericException struct and ToGenericException converter 
(#397)
28ed891 is described below

commit 28ed8912f0268d9a9775bb16bc647c68cde09ccf
Author: Modo <[email protected]>
AuthorDate: Thu Jul 16 18:42:56 2026 +0800

    Add: GenericException struct and ToGenericException converter (#397)
    
    * add GenericException definition and conversion function
    
    * add test and preserve exception class in parseLegacyException
    
    * fix: split import block in new test
    
    * fix: guard typed-nil pointers in ToGenericException type switch and add 
typed-nil tests
    
    * add documentation comment for GenericException
---
 generic_exception.go      | 100 +++++++++++++++++++++++
 generic_exception_test.go | 201 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 301 insertions(+)

diff --git a/generic_exception.go b/generic_exception.go
new file mode 100644
index 0000000..7604c09
--- /dev/null
+++ b/generic_exception.go
@@ -0,0 +1,100 @@
+/*
+ * 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 hessian
+
+import (
+       "reflect"
+       "strings"
+)
+
+import (
+       "github.com/apache/dubbo-go-hessian2/java_exception"
+)
+
+// GenericException represents a Java exception with its class name and 
message.
+//
+// Previously, when dubbo-go invoked a Java service and the Java side threw
+// a business exception (e.g. UserNotFoundException), the original exception
+// type was lost — the Go caller only received a plain string error message
+// and could not determine which specific exception was thrown.
+//
+// This type preserves the full exception information:
+//   - ExceptionClass:  the fully qualified Java class name, e.g. 
"com.example.UserNotFoundException"
+//   - ExceptionMessage: the exception detail message, e.g. "user not found"
+//
+// See also:
+//   - Java dubbo's GenericException.java 
(dubbo-common/src/main/java/org/apache/dubbo/rpc/service/GenericException.java)
+//   - GitHub issue: https://github.com/apache/dubbo-go/issues/3167
+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:
+               if v == nil {
+                       return nil, false
+               }
+               return v, true
+       case GenericException:
+               return &v, true
+       case *java_exception.DubboGenericException:
+               if v == nil {
+                       return nil, false
+               }
+               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:
+               if rv := reflect.ValueOf(v); rv.Kind() == reflect.Ptr && 
rv.IsNil() {
+                       return nil, false
+               }
+               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))
+               if class, message, ok := strings.Cut(msg, " - "); ok {
+                       return &GenericException{
+                               ExceptionClass:   strings.TrimSpace(class),
+                               ExceptionMessage: strings.TrimSpace(message),
+                       }
+               }
+       }
+       return &GenericException{ExceptionClass: "java.lang.Exception", 
ExceptionMessage: msg}
+}
diff --git a/generic_exception_test.go b/generic_exception_test.go
new file mode 100644
index 0000000..86a7f12
--- /dev/null
+++ b/generic_exception_test.go
@@ -0,0 +1,201 @@
+/*
+ * 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 hessian
+
+import (
+       "testing"
+)
+
+import (
+       "github.com/stretchr/testify/assert"
+)
+
+import (
+       "github.com/apache/dubbo-go-hessian2/java_exception"
+)
+
+func TestParseLegacyException(t *testing.T) {
+       tests := []struct {
+               name        string
+               input       string
+               wantClass   string
+               wantMessage string
+       }{
+               {
+                       name:        "plain string",
+                       input:       "user not found",
+                       wantClass:   "java.lang.Exception",
+                       wantMessage: "user not found",
+               },
+               {
+                       name:        "legacy prefix without separator",
+                       input:       "java exception: user not found",
+                       wantClass:   "java.lang.Exception",
+                       wantMessage: "user not found",
+               },
+               {
+                       name:        "full Error() format",
+                       input:       "java exception: com.example.FooException 
- something went wrong",
+                       wantClass:   "com.example.FooException",
+                       wantMessage: "something went wrong",
+               },
+               {
+                       name:        "message with separator inside",
+                       input:       "java exception: com.example.FooException 
- error - more details",
+                       wantClass:   "com.example.FooException",
+                       wantMessage: "error - more details",
+               },
+               {
+                       name:        "prefix with trailing spaces",
+                       input:       "  java exception:  
com.example.FooException  -  something went wrong  ",
+                       wantClass:   "com.example.FooException",
+                       wantMessage: "something went wrong",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       ge := parseLegacyException(tt.input)
+                       assert.Equal(t, tt.wantClass, ge.ExceptionClass)
+                       assert.Equal(t, tt.wantMessage, ge.ExceptionMessage)
+               })
+       }
+}
+
+func TestGenericExceptionError(t *testing.T) {
+       tests := []struct {
+               name      string
+               class     string
+               message   string
+               wantError string
+       }{
+               {
+                       name:      "class and message",
+                       class:     "com.example.FooException",
+                       message:   "something went wrong",
+                       wantError: "java exception: com.example.FooException - 
something went wrong",
+               },
+               {
+                       name:      "message only",
+                       message:   "user not found",
+                       wantError: "user not found",
+               },
+               {
+                       name:      "class only",
+                       class:     "com.example.FooException",
+                       wantError: "com.example.FooException",
+               },
+               {
+                       name:      "empty both",
+                       wantError: "",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       e := GenericException{ExceptionClass: tt.class, 
ExceptionMessage: tt.message}
+                       assert.Equal(t, tt.wantError, e.Error())
+               })
+       }
+}
+
+func TestToGenericException(t *testing.T) {
+       t.Run("from *GenericException", func(t *testing.T) {
+               ge := &GenericException{ExceptionClass: "com.example.Foo", 
ExceptionMessage: "bar"}
+               got, ok := ToGenericException(ge)
+               assert.True(t, ok)
+               assert.Same(t, ge, got)
+       })
+
+       t.Run("from GenericException value", func(t *testing.T) {
+               ge := GenericException{ExceptionClass: "com.example.Foo", 
ExceptionMessage: "bar"}
+               got, ok := ToGenericException(ge)
+               assert.True(t, ok)
+               assert.Equal(t, ge.ExceptionClass, got.ExceptionClass)
+               assert.Equal(t, ge.ExceptionMessage, got.ExceptionMessage)
+       })
+
+       t.Run("from *DubboGenericException", func(t *testing.T) {
+               dge := &java_exception.DubboGenericException{
+                       ExceptionClass:   "com.example.Foo",
+                       ExceptionMessage: "bar",
+               }
+               got, ok := ToGenericException(dge)
+               assert.True(t, ok)
+               assert.Equal(t, "com.example.Foo", got.ExceptionClass)
+               assert.Equal(t, "bar", got.ExceptionMessage)
+       })
+
+       t.Run("from DubboGenericException value", func(t *testing.T) {
+               dge := java_exception.DubboGenericException{
+                       ExceptionClass:   "com.example.Foo",
+                       ExceptionMessage: "bar",
+               }
+               got, ok := ToGenericException(dge)
+               assert.True(t, ok)
+               assert.Equal(t, "com.example.Foo", got.ExceptionClass)
+               assert.Equal(t, "bar", got.ExceptionMessage)
+       })
+
+       t.Run("from Throwabler", func(t *testing.T) {
+               thr := java_exception.NewThrowable("some error")
+               got, ok := ToGenericException(thr)
+               assert.True(t, ok)
+               assert.Equal(t, thr.JavaClassName(), got.ExceptionClass)
+               assert.Equal(t, thr.Error(), got.ExceptionMessage)
+       })
+
+       t.Run("from string", func(t *testing.T) {
+               got, ok := ToGenericException("user not found")
+               assert.True(t, ok)
+               assert.Equal(t, "java.lang.Exception", got.ExceptionClass)
+               assert.Equal(t, "user not found", got.ExceptionMessage)
+       })
+
+       t.Run("from string with Error() format", func(t *testing.T) {
+               got, ok := ToGenericException("java exception: com.example.Foo 
- something went wrong")
+               assert.True(t, ok)
+               assert.Equal(t, "com.example.Foo", got.ExceptionClass)
+               assert.Equal(t, "something went wrong", got.ExceptionMessage)
+       })
+
+       t.Run("from unsupported type", func(t *testing.T) {
+               got, ok := ToGenericException(42)
+               assert.False(t, ok)
+               assert.Nil(t, got)
+       })
+
+       t.Run("typed nil *GenericException", func(t *testing.T) {
+               got, ok := ToGenericException((*GenericException)(nil))
+               assert.False(t, ok)
+               assert.Nil(t, got)
+       })
+
+       t.Run("typed nil *DubboGenericException", func(t *testing.T) {
+               got, ok := 
ToGenericException((*java_exception.DubboGenericException)(nil))
+               assert.False(t, ok)
+               assert.Nil(t, got)
+       })
+
+       t.Run("typed nil Throwabler", func(t *testing.T) {
+               var thr java_exception.Throwabler = 
(*java_exception.Throwable)(nil)
+               got, ok := ToGenericException(thr)
+               assert.False(t, ok)
+               assert.Nil(t, got)
+       })
+}

Reply via email to