Summary
Neither RMQ gateway models an invalid-message destination. A rejection carrying
RejectionReason.UnacceptableMessage dead-letters to the DLQ along with delivery errors, so the two
rejection classes are indistinguishable at the destination.
This holds conformance behaviour FR-5 (reject with unacceptable reason routes to the invalid
channel) Deferred on three cells: RMQ.Async / Classic, RMQ.Async / Quorum and
RMQ.Sync / RmqSyncMessagingGateway. Split out of the #4240 umbrella so it is tracked on its own.
Evidence
From the conformance run against a live RabbitMQ 4.2 broker, both variants: the provider's real
invalid-channel read hook (not a Message.Empty stub) observes MT_NONE after an unacceptable
rejection, while the DLQ read hook observes the message.
specs/0036-universal-transport-conformance-tests/conformance-status.md records the measurement and the
three deferral preconditions being met.
Cause
grep -rn --include='*.cs' 'InvalidMessageRoutingKey' src/Paramore.Brighter.MessagingGateway.RMQ.Async/ src/Paramore.Brighter.MessagingGateway.RMQ.Sync/ returns nothing. Neither RmqSubscription nor
RmqMessageConsumer carries an invalid destination, and neither package implements
IUseBrighterInvalidMessageSupport.
RMQ's rejection path is a native BasicReject, which dead-letters through the single configured DLX
(x-dead-letter-exchange / x-dead-letter-routing-key). One exchange, one destination — the reason
cannot influence where the message lands.
This is a routing gap, not a metadata gap, so it is not covered by the maintainer-approved FR-8
relaxation (which lets a natively dead-lettering transport conform on routing alone). FR-4, FR-6 and
FR-17 all Pass for RMQ because the DLQ is the right destination for those; FR-5 is the one case where
a second destination is required and none exists.
Suggested direction
Brighter-managed invalid routing in src/Paramore.Brighter.MessagingGateway.RMQ.Async (and RMQ.Sync):
RmqSubscription implements IUseBrighterInvalidMessageSupport, the consumer factory wires an
invalid-message producer, and RejectAsync republishes to the invalid routing key when
reason == UnacceptableMessage before BasicReject-ing the original — the shape the nine gateways that
already implement the interface use (RocketMQ, AWSSQS ×2, Redis, Postgres, Kafka, MsSql, MQTT).
Note that RMQ is also the transport where RejectionMetadataKeys are empty, so a Brighter-managed
invalid path would additionally let RMQ stamp real rejection metadata rather than relying on the
relaxation.
Contrast Azure Service Bus, whose FR-5 deferral is a genuine platform difference and is not in
scope here: ASB's DLQ is a built-in sub-queue and there is no second entity to route to.
Scope
- 3 ledger cells:
RMQ.Async / Classic FR-5, RMQ.Async / Quorum FR-5, RMQ.Sync FR-5
- Repoint those cells from
Deferred -> #4240 to this issue
Related
Summary
Neither RMQ gateway models an invalid-message destination. A rejection carrying
RejectionReason.UnacceptableMessagedead-letters to the DLQ along with delivery errors, so the tworejection classes are indistinguishable at the destination.
This holds conformance behaviour FR-5 (reject with unacceptable reason routes to the invalid
channel)
Deferredon three cells:RMQ.Async / Classic,RMQ.Async / QuorumandRMQ.Sync / RmqSyncMessagingGateway. Split out of the #4240 umbrella so it is tracked on its own.Evidence
From the conformance run against a live RabbitMQ 4.2 broker, both variants: the provider's real
invalid-channel read hook (not a
Message.Emptystub) observesMT_NONEafter an unacceptablerejection, while the DLQ read hook observes the message.
specs/0036-universal-transport-conformance-tests/conformance-status.mdrecords the measurement and thethree deferral preconditions being met.
Cause
grep -rn --include='*.cs' 'InvalidMessageRoutingKey' src/Paramore.Brighter.MessagingGateway.RMQ.Async/ src/Paramore.Brighter.MessagingGateway.RMQ.Sync/returns nothing. NeitherRmqSubscriptionnorRmqMessageConsumercarries an invalid destination, and neither package implementsIUseBrighterInvalidMessageSupport.RMQ's rejection path is a native
BasicReject, which dead-letters through the single configured DLX(
x-dead-letter-exchange/x-dead-letter-routing-key). One exchange, one destination — the reasoncannot influence where the message lands.
This is a routing gap, not a metadata gap, so it is not covered by the maintainer-approved FR-8
relaxation (which lets a natively dead-lettering transport conform on routing alone). FR-4, FR-6 and
FR-17 all
Passfor RMQ because the DLQ is the right destination for those; FR-5 is the one case wherea second destination is required and none exists.
Suggested direction
Brighter-managed invalid routing in
src/Paramore.Brighter.MessagingGateway.RMQ.Async(and RMQ.Sync):RmqSubscriptionimplementsIUseBrighterInvalidMessageSupport, the consumer factory wires aninvalid-message producer, and
RejectAsyncrepublishes to the invalid routing key whenreason == UnacceptableMessagebeforeBasicReject-ing the original — the shape the nine gateways thatalready implement the interface use (RocketMQ, AWSSQS ×2, Redis, Postgres, Kafka, MsSql, MQTT).
Note that RMQ is also the transport where
RejectionMetadataKeysare empty, so a Brighter-managedinvalid path would additionally let RMQ stamp real rejection metadata rather than relying on the
relaxation.
Contrast Azure Service Bus, whose FR-5 deferral is a genuine platform difference and is not in
scope here: ASB's DLQ is a built-in sub-queue and there is no second entity to route to.
Scope
RMQ.Async / ClassicFR-5,RMQ.Async / QuorumFR-5,RMQ.SyncFR-5Deferred -> #4240to this issueRelated