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
- Re-copy the handle exactly as delivered by the broker/send result, without trimming, decoding, or re-encoding.
- If the handle traveled through a URL or JSON layer, ensure lossless round-trip (URL-encode as one token, treat as opaque string).
- Verify the client version matches the broker that produced the handle so HandleV1 layout agrees.
- 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
- Treat recall handles as opaque: never trim, decode, or re-encode them in transit.
- Use URL-encoding when handles cross HTTP boundaries to survive '+' and '=' mangling.
- Keep client and broker versions in sync so HandleV1 wire format matches.
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
- topic is not supported
- The broker service address not found
- 13
- Sending message to topic[%s] is forbidden.
- sendMessage call timeout
AI-assisted analysis of apache/rocketmq@293f588571 (2026-08-14).
Data as JSON: /api/errors/3983a22194e28f4f.
Report an issue: GitHub.