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)
+ })
+}