Why Dubbo Async Calls Return False for boolean? A Deep Dive and Fix

An in‑depth investigation reveals why Dubbo’s asynchronous calls return false when the service method returns a primitive boolean, reproduces the issue with a demo, traces the problem to the asyncCall implementation, and proposes a code fix to prevent this subtle bug.

Xiao Lou's Tech Notes
Xiao Lou's Tech Notes
Xiao Lou's Tech Notes
Why Dubbo Async Calls Return False for boolean? A Deep Dive and Fix

Problem Reproduction

A colleague reported a Dubbo async‑call issue where a method returning boolean gave true on the server side but false on the client, while changing the return type to Boolean worked correctly.

今天发现一个问题 有一个dubbo接口返回类型是boolean, 把接口从同步改成异步 server 端返回true 消费端却返回false,把boolean改成Boolean就能正常返回结果 有碰到过这个问题吗

Interface return type is boolean Switching from sync to async changes the returned value

Changing to the wrapper type Boolean fixes the problem

The Java Development Manual recommends using wrapper types for RPC interfaces, but the observed behavior suggests a bug in Dubbo’s async handling.

Demo Code

public interface DemoService {
    boolean isUser();
    Boolean isFood();
}
@Service
public class DemoServiceImpl implements DemoService {
    @Override
    public boolean isUser() {
        System.out.println("server is user : true");
        return true;
    }
    @Override
    public Boolean isFood() {
        System.out.println("server is food : true");
        return true;
    }
}
@RestController
public class DemoCallerService {
    @Reference(injvm = false, check = false)
    private DemoService demoService;

    @GetMapping(path = "/isUser")
    public String isUser() throws Exception {
        BlockingQueue<Boolean> q = new ArrayBlockingQueue<>(1);
        RpcContext.getContext().asyncCall(() -> demoService.isUser())
            .handle((isUser, throwable) -> {
                System.out.println("client is user = " + isUser);
                q.add(isUser);
                return isUser;
            });
        q.take();
        return "ok";
    }

    @GetMapping(path = "/isFood")
    public String isFood() throws Exception {
        BlockingQueue<Boolean> q = new ArrayBlockingQueue<>(1);
        RpcContext.getContext().asyncCall(() -> demoService.isFood())
            .handle((isFood, throwable) -> {
                System.out.println("client is food = " + isFood);
                q.add(isFood);
                return isFood;
            });
        q.take();
        return "ok";
    }
}

Test Results

Calling isUser (returns primitive boolean) prints:

// client ...
client is user = false
// server ...
server is user : true

Calling isFood (returns wrapper Boolean) prints:

// client ...
client is food = true
// server ...
server is food : true

Investigation

The first guess was a server‑side conversion error, but the client actually receives the wrong value. The call stack leads to

com.alibaba.dubbo.remoting.exchange.support.DefaultFuture#doReceived

. In async mode Dubbo wraps the call in a Callable. If the callable returns a non‑null value that is not a CompletableFuture, Dubbo immediately completes the future with CompletableFuture.completedFuture(o).

Because the proxy’s invoke method returns the primitive boolean result directly, the value false (the default for a primitive) is returned before the remote response arrives, and the future is completed with false.

Using Arthas to inspect the generated proxy class ( *.proxy0) confirms that the this.handler.invoke call returns false for the primitive case, while the wrapper type correctly yields a CompletableFuture.

The problematic code in asyncCall contains a comment “local invoke will return directly”. For non‑injvm (remote) calls this shortcut should be skipped, otherwise primitive returns are forced to false.

Fix

The proposed fix adds a condition that only completes the future immediately when the call is an injvm (local) invocation. For remote async calls the method should fall through to the normal future handling.

public <T> CompletableFuture<T> asyncCall(Callable<T> callable) {
    try {
        setAttachment(ASYNC_KEY, Boolean.TRUE.toString());
        T o = callable.call();
        if (o != null) {
            if (o instanceof CompletableFuture) {
                return (CompletableFuture<T>) o;
            }
            if (injvm()) { // keep current behavior for local calls
                return CompletableFuture.completedFuture(o);
            }
        } else {
            // normal sync method, get future from RpcContext
        }
    } catch (Exception e) {
        throw new RpcException(e);
    } finally {
        removeAttachment(ASYNC_KEY);
    }
    return (CompletableFuture<T>) getContext().getFuture();
}

Conclusion

The bug exists in Dubbo 2.7.4 and later; submitting a PR with the above change is encouraged. Following the guideline of using wrapper types for RPC return values avoids this subtle issue, and thorough debugging tools like Arthas help uncover such hidden problems.

booleanasync-callbug-fix
Xiao Lou's Tech Notes
Written by

Xiao Lou's Tech Notes

Backend technology sharing, architecture design, performance optimization, source code reading, troubleshooting, and pitfall practices

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.