apache/rocketmq · error · MQClientException

{} (dynamic: cause message from RecallMessageHandle.decodeHa

Error message

{} (dynamic: cause message from RecallMessageHandle.decodeHandle failure)

What it means

Dynamic MQClientException whose message and null-cause come from an exception raised while RecallMessageHandle.decodeHandle(recallHandle) parsed the handle (the '{}' placeholder is the parse error text; the annotated suffix marks it as dynamic). The recall handle is a broker-issued opaque token that must be base64/struct-decodable into HandleV1; anything undecodable fails here before any network call.

Source

Thrown at client/src/main/java/org/apache/rocketmq/client/impl/producer/DefaultMQProducerImpl.java:1582

        requestHeader.setMsgId(sendResult.getMsgId());
        String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null;
        this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark,
            this.defaultMQProducer.getSendMsgTimeout());
    }

    public String recallMessage(
        String topic,
        String recallHandle) throws RemotingException, MQClientException, MQBrokerException, InterruptedException {
        makeSureStateOK();
        Validators.checkTopic(topic);
        if (NamespaceUtil.isRetryTopic(topic) || NamespaceUtil.isDLQTopic(topic)) {
            throw new MQClientException("topic is not supported", null);
        }
        RecallMessageHandle.HandleV1 handleEntity;
        try {
            handleEntity = (RecallMessageHandle.HandleV1) RecallMessageHandle.decodeHandle(recallHandle);
        } catch (Exception e) {
            throw new MQClientException(e.getMessage(), null);
        }

        tryToFindTopicPublishInfo(topic);
        String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(handleEntity.getBrokerName());
        brokerAddr = StringUtils.isNotEmpty(brokerAddr) ?
            // find another address to support multi proxy endpoints,
            // may cause failure request in proxy-less mode when the broker is temporarily unavailable
            brokerAddr : this.mQClientFactory.findBrokerAddrByTopic(topic);
        if (StringUtils.isEmpty(brokerAddr)) {
            log.warn("can't find broker service address. {}", handleEntity.getBrokerName());
            throw new MQClientException("The broker service address not found", null);
        }
        RecallMessageRequestHeader requestHeader = new RecallMessageRequestHeader();
        requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
        requestHeader.setTopic(topic);
        requestHeader.setRecallHandle(recallHandle);
        requestHeader.setBrokerName(handleEntity.getBrokerName());
        return this.mQClientFactory.getMQClientAPIImpl().recallMessage(brokerAddr,

View on GitHub (pinned to 293f588571)

Solutions

  1. Re-copy the handle exactly as delivered by the broker/send result, without trimming, decoding, or re-encoding.
  2. If the handle traveled through a URL or JSON layer, ensure lossless round-trip (URL-encode as one token, treat as opaque string).
  3. Verify the client version matches the broker that produced the handle so HandleV1 layout agrees.
  4. Add a pre-check that the handle is non-empty and plausibly base64 before calling recallMessage, surfacing a clear validation error to users.

Example fix

// before
String handle = params.get("handle").replace(" ", "+"); // corrupted transit
producer.recallMessage(topic, handle);

// after
String handle = params.get("handle"); // transported URL-encoded, decoded exactly once by the framework
if (handle == null || handle.isBlank()) throw new IllegalArgumentException("recall handle required");
producer.recallMessage(topic, handle);
Defensive patterns

Strategy: validation

Validate before calling

// handle must be non-blank and plausibly base64 before calling recallMessage
if (recallHandle == null || recallHandle.isBlank()
        || !recallHandle.matches("[A-Za-z0-9+/=_-]+")) {
    throw new IllegalArgumentException("malformed recall handle");
}

Type guard

static boolean isPlausibleRecallHandle(String h) {
    return h != null && !h.isBlank() && h.matches("[A-Za-z0-9+/=_-]+");
}

Try / catch

try {
    producer.recallMessage(topic, recallHandle);
} catch (MQClientException e) {
    // decode failures surface with the parser's message and null cause
    log.warn("recall handle rejected for topic {}: {}", topic, e.getMessage());
    reRequestHandleFromSource(); // ask the issuing system for a fresh handle
}

Prevention

When it happens

Trigger: Passing a malformed, truncated, or tampered recallHandle string; passing a handle from an incompatible RocketMQ version whose HandleV1 encoding differs; URL-decoding/escaping corruption (e.g. '+' vs space) when the handle transited a query parameter or log pipeline.

Common situations: Copying handles out of logs/consoles with whitespace or quoting damage; handles forwarded through HTTP APIs where encoding is altered; version skew between the broker that issued the handle and the client decoding it.

Related errors


AI-assisted analysis of apache/rocketmq@293f588571 (2026-08-14). Data as JSON: /api/errors/3983a22194e28f4f. Report an issue: GitHub.