apache/rocketmq · error · MQClientException

Sending message to topic[%s] is forbidden.

Error message

Sending message to topic[%s] is forbidden.

What it means

Validators.isNotAllowedSendTopic throws when the topic is on TopicValidator's forbidden-to-send list — %RETRY%* and %DLQ%* topics. Retry and DLQ topics are consumed, never published to directly; sending would corrupt retry semantics, so the client blocks it.

Source

Thrown at client/src/main/java/org/apache/rocketmq/client/Validators.java:121

        }

        if (isTopicOrGroupIllegal(topic)) {
            throw new MQClientException(String.format(
                    "The specified topic[%s] contains illegal characters, allowing only %s", topic,
                    "^[%|a-zA-Z0-9_-]+$"), null);
        }
    }

    public static void isSystemTopic(String topic) throws MQClientException {
        if (TopicValidator.isSystemTopic(topic)) {
            throw new MQClientException(
                    String.format("The topic[%s] is conflict with system topic.", topic), null);
        }
    }

    public static void isNotAllowedSendTopic(String topic) throws MQClientException {
        if (TopicValidator.isNotAllowedSendTopic(topic)) {
            throw new MQClientException(
                    String.format("Sending message to topic[%s] is forbidden.", topic), null);
        }
    }

    public static void checkTopicConfig(final TopicConfig topicConfig) throws MQClientException {
        if (!PermName.isValid(topicConfig.getPerm())) {
            throw new MQClientException(ResponseCode.NO_PERMISSION,
                String.format("topicPermission value: %s is invalid.", topicConfig.getPerm()));
        }
    }

    public static void checkBrokerConfig(final Properties brokerConfig) throws MQClientException {
        String brokerPermission = brokerConfig.getProperty("brokerPermission");
        if (brokerPermission != null && !PermName.isValid(brokerPermission)) {
            throw new MQClientException(ResponseCode.NO_PERMISSION,
                    String.format("brokerPermission value: %s is invalid.", brokerPermission));
        }
    }

View on GitHub (pinned to 293f588571)

Solutions

  1. Send to your own business topic; to re-drive failures, resend the original message to its original topic.
  2. Consume DLQ with a consumer subscribed to '%DLQ%group' instead of producing to it.
  3. For delayed retry semantics, rely on the consumer's suspendCurrentQueueTimeMillis/retryTimesWhenConsumeFailed or schedule a delayed message to the original topic.

Example fix

// before
producer.send(new Message("%DLQ%order-group", tag, body)); // forbidden

// after
producer.send(new Message("order-topic", tag, body)); // resend to original topic
Defensive patterns

Strategy: validation

Validate before calling

static boolean sendableTopic(String topic) {
    return !org.apache.rocketmq.common.topic.TopicValidator.isNotAllowedSendTopic(topic);
}

Prevention

When it happens

Trigger: producer.send(new Message("%DLQ%order-group", ...)) or sending to '%RETRY%consumer-group'; checkMessage calls isNotAllowedSendTopic for every outbound message.

Common situations: Dead-letter handling reimplemented by writing to the DLQ topic manually; consumers that 'requeue' by producing into the retry topic; config that accidentally prefixes a business topic with %RETRY%.

Understand the failure class

Related errors


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