apache/dubbo · error · RpcException

INTERNAL_ERROR

INTERNAL_ERROR

Error message

The result of filter invocation must be AsyncRpcResult. (If you want to recreate a result, please use AsyncRpcResult.newDefaultActionResult.) Filter class: {filterClass}. Result class: {resultClass}.

What it means

Thrown by the cluster FilterChainBuilder node when a Filter.invoke(...) returns a Result that is not an AsyncRpcResult. Dubbo's filter chain is built around async results: every filter (cluster-scoped or provider/consumer-scoped) must return an AsyncRpcResult. Returning any other Result type (e.g., a raw AppResponse or a custom Result) breaks the async contract and is rejected (logged as INTERNAL_ERROR).

Source

Thrown at dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/filter/FilterChainBuilder.java:356

        @Override
        public boolean isAvailable() {
            return originalInvoker.isAvailable();
        }

        @Override
        public Result invoke(Invocation invocation) throws RpcException {
            Result asyncResult;
            try {
                InvocationProfilerUtils.enterDetailProfiler(
                        invocation, () -> "Filter " + filter.getClass().getName() + " invoke.");
                asyncResult = filter.invoke(nextNode, invocation);
                if (!(asyncResult instanceof AsyncRpcResult)) {
                    String msg =
                            "The result of filter invocation must be AsyncRpcResult. (If you want to recreate a result, please use AsyncRpcResult.newDefaultAsyncResult.) "
                                    + "Filter class: " + filter.getClass().getName() + ". Result class: "
                                    + asyncResult.getClass().getName() + ".";
                    LOGGER.error(INTERNAL_ERROR, "", "", msg);
                    throw new RpcException(msg);
                }
            } catch (Exception e) {
                InvocationProfilerUtils.releaseDetailProfiler(invocation);
                if (filter instanceof ListenableFilter) {
                    ListenableFilter listenableFilter = ((ListenableFilter) filter);
                    try {
                        Filter.Listener listener = listenableFilter.listener(invocation);
                        if (listener != null) {
                            listener.onError(e, originalInvoker, invocation);
                        }
                    } finally {
                        listenableFilter.removeListener(invocation);
                    }
                } else if (filter instanceof FILTER.Listener) {
                    FILTER.Listener listener = (FILTER.Listener) filter;
                    listener.onError(e, originalInvoker, invocation);
                }
                throw e;

View on GitHub (pinned to 3a3043227f)

Solutions

  1. Return AsyncRpcResult from filter.invoke(); when synthesizing a result use AsyncRpcResult.newDefaultAsyncResult(value) or newDefaultAsyncResult(exception).
  2. Pass through the downstream result unchanged unless you intentionally replace it, and when replacing always produce an AsyncRpcResult.
  3. Audit custom filters against the post-2.7 Filter contract; the message names the offending filter class.

Example fix

// before (broken): custom filter returns a plain result
public Result invoke(Invoker<?> invoker, Invocation inv) {
    if (cached != null) return new AppResponse(cached); // not AsyncRpcResult -> throws
    return invoker.invoke(inv);
}

// after
public Result invoke(Invoker<?> invoker, Invocation inv) {
    if (cached != null) return AsyncRpcResult.newDefaultAsyncResult(cached);
    return invoker.invoke(inv);
}
Defensive patterns

Strategy: type-guard

Validate before calling

// In a custom filter, always produce an AsyncRpcResult
public Result invoke(Invoker<?> invoker, Invocation inv) {
    Result r = invoker.invoke(inv);
    if (!(r instanceof AsyncRpcResult) && shouldTransform(r)) {
        return AsyncRpcResult.newDefaultAsyncResult(r.getValue());
    }
    return r;
}

Type guard

// Type guard for filter results
static boolean isAsyncResult(Result r) {
    return r instanceof AsyncRpcResult;
}

Try / catch

try {
    return filter.invoke(nextNode, invocation);
} catch (RpcException e) {
    if (e.getMessage().contains("must be AsyncRpcResult")) {
        // filter returned a wrong type; wrap/replace with AsyncRpcResult
        return AsyncRpcResult.newDefaultAsyncResult(e);
    }
    throw e;
}

Prevention

When it happens

Trigger: A custom Filter (or a filter extending ListenableFilter / implementing Filter with an onResponse callback) whose invoke() constructs and returns its own Result via 'new AppResponse(...)' or 'new RpcResult(...)' instead of going through AsyncRpcResult.newDefaultAsyncResult(...).

Common situations: Writing a custom Dubbo Filter that short-circuits (e.g., a cache filter returning a cached value) and building the Result manually; porting a pre-2.7 filter to a newer Dubbo version where AsyncRpcResult is mandatory; a filter that wraps/swallows the downstream result incorrectly.

Related errors


AI-assisted analysis of apache/dubbo@3a3043227f (2026-08-14). Data as JSON: /api/errors/8ca96c1700d85bc3. Report an issue: GitHub.