Skip to content

[Bug] Async wildcard and generic-array return types fail reflection and produce incorrect payload metadata #16501

Open

Description

@hickey-ai

Pre-check

  • I am sure that all the content I provide is in English.

Search before asking

  • I had searched in the issues and found no similar issues.

Apache Dubbo Component

Java SDK (apache/dubbo)

Dubbo Version

Apache Dubbo source: 3.3.7-SNAPSHOT, branch 3.3, commit ae809de2fc81b408194240965eb20bf27e6971df.
Windows, OpenJDK 21, Maven 3.6.3.

Verification used an isolated Maven/JUnit harness: eight relevant implementation/interface files were copied byte-for-byte from this commit, with released Dubbo 3.3.6 artifacts supplying supporting dependencies. The paths under review were compiled from the current source, not taken from those dependency jars. This is not a full current-branch reactor build or a networked RPC deployment.

Steps to reproduce this issue

Legal asynchronous return signatures containing a wildcard or a generic array throw ClassCastException in ReflectUtils.getReturnTypes. ReflectionMethodDescriptor catches the exception but then records CompletableFuture itself as the response type, losing the actual payload type. A plain CompletableFuture control succeeds.

Executed JUnit Jupiter reproducer:

import java.lang.reflect.Type;
import java.util.concurrent.CompletableFuture;
import org.apache.dubbo.common.utils.ReflectUtils;
import org.apache.dubbo.rpc.model.ReflectionMethodDescriptor;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class AsyncReturnTypesTest {
    interface Api<T> {
        CompletableFuture<String> plain();
        CompletableFuture<? extends CharSequence> wildcard();
        CompletableFuture<T[]> array();
    }

    @Test void plainControl() throws Exception {
        assertEquals(String.class,
            ReflectUtils.getReturnTypes(Api.class.getMethod("plain"))[0]);
    }

    @Test void wildcardMustNotThrow() {
        Type[] result = assertDoesNotThrow(() ->
            ReflectUtils.getReturnTypes(Api.class.getMethod("wildcard")));
        assertEquals(CharSequence.class, result[0]);
    }

    @Test void genericArrayMustNotThrow() {
        Type[] result = assertDoesNotThrow(() ->
            ReflectUtils.getReturnTypes(Api.class.getMethod("array")));
        assertEquals(Object[].class, result[0]);
    }

    @Test void descriptorMustNotUseFutureAsPayloadType() throws Exception {
        ReflectionMethodDescriptor descriptor =
            new ReflectionMethodDescriptor(Api.class.getMethod("wildcard"));
        assertEquals(CharSequence.class, descriptor.getReturnTypes()[0]);
    }
}

What you expected to happen

Resolve the raw payload class/bounds without casting arbitrary java.lang.reflect.Type instances to Class; preserve the generic payload type where possible. For these signatures the raw types are CharSequence and Object[]. The descriptor should not silently substitute CompletableFuture for the payload.

Observed results:

WildcardTypeImpl cannot be cast to java.lang.Class
  at ReflectUtils.getReturnTypes(ReflectUtils.java:1188)
GenericArrayTypeImpl cannot be cast to java.lang.Class
  at ReflectUtils.getReturnTypes(ReflectUtils.java:1188)
wildcard descriptor response: class java.util.concurrent.CompletableFuture
expected: <java.lang.CharSequence> but was: <java.util.concurrent.CompletableFuture>

The plain CompletableFuture control passed. Three regressions failed as shown above.

Anything else

Root-cause locations: ReflectUtils.getReturnTypes and ReflectionMethodDescriptor constructor. The fallback branch assumes a Future type argument is a Class when it is neither ParameterizedType nor TypeVariable; WildcardType and GenericArrayType also occur in ordinary Java signatures.

These are src/main/java implementations used for RPC method metadata, not test utilities. I verified the reflection/descriptor path only; a complete exported service and wire deserialization scenario was not tested.

Searched getReturnTypes, WildcardType/Future and GenericArrayType/Future reports. #7040 covered the older direct TypeVariable case; the current code handles that case, while the wildcard/generic-array cases above still fail. A possible fix is recursive raw-type resolution with explicit handling for wildcard bounds and generic arrays, with regression coverage for both and the plain Future control.

Do you have a (mini) reproduction demo?

  • Yes, I have a minimal reproduction demo to help resolve this issue more effectively!

Are you willing to submit a pull request to fix on your own?

  • Yes I am willing to submit a pull request on my own!

Code of Conduct

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions