Skip to content

[RabbitMQ] Deserialization failure in request/response consumer silently discards message without sending error response to requester #480

Description

@quantum33

Hello @zarusz

Here's the description:

Context

When using the request/response (RPC) pattern on RabbitMQ with ConsumerMode.RequestResponse,
a deserialization failure on the handler queue silently discards the message (NACK without requeue)
but sends no response to the requester. The requester waits until its timeout expires with
no indication of what went wrong.


Current behaviour

The exception is thrown inside _messageProvider(messageType, messageHeaders, transportMessage)
in MessageProcessor<T>.ProcessMessage. The outer catch block catches it, sets
result = ProcessResult.Failure, and returns. The transport layer then NACKs the message.

Because the exception occurs before DoHandle is reached, neither the
IRabbitMqConsumerErrorHandler<T> nor any consumer-level error handler is ever invoked.
The handler's OnHandle method is never called, so there is no opportunity to send a
typed failure response to the requester.

_messageProvider(messageType, ...) ← throws JsonSerializationException
outer catch → result = Failure → NACK
DoHandle(message) ← never reached
└─ IRabbitMqConsumerErrorHandler ← never reached
└─ handler.OnHandle ← never reached → no response sent to requester

Expected behaviour

For request/response consumers, a deserialization failure should result in an error response
being sent back to the requester (e.g. via IResponseProducer.ProduceResponse with the
deserialization exception). This would:

  • prevent the requester from timing out with no feedback
  • allow user code (via IRabbitMqConsumerErrorHandler<T> or equivalent) to decide on
    the broker disposition (ACK or NACK)

Important constraint — Requeue must be excluded

A deserialization failure is a permanent failure: the message payload will never
deserialize successfully regardless of how many times it is retried. Allowing Requeue
as a valid response would produce an infinite retry loop.
The available actions should be limited to NACK (default) or ACK (silent discard).


Steps to reproduce

  1. Configure a RabbitMQ request/response handler for TRequest where one field is typed as an enum.
  2. Publish a raw AMQP message with an unknown string value for that enum field
    (e.g. {"ask_for": "unknown.value"}).
  3. Observe: JsonSerializationException logged, message NACKed,
    requester never receives a response and times out.
  4. Expected: requester receives a typed error response; message is NACKed
    (or ACKed if the error handler decides so).

Additional observation — second identical message stayed unacked

During testing, sending a second identical malformed message produced a different
outcome: the deserialization failure occurred identically, but the message was
not NACKed and remained unacked indefinitely.

The message was only resolved when the channel was reset — either by a broker
maintenance event (CONNECTION_FORCED) or by restarting the consumer. In both
cases, RabbitMQ redelivered the unacked message, which then fell back into the
main scenario (deserialization failure → NACK → discarded, still no RPC response).

The root cause of the inconsistency between the first and second message (one
NACKed, one not) is unclear — it may be related to channel state after the first
failure.

Side effect: if basicQos prefetch count is low (e.g. 1), a stuck unacked
message blocks all subsequent deliveries to that consumer for the duration of
the window.


Version

SlimMessageBus 3.5.0 (also present in 3.4.0), transport SlimMessageBus.Host.RabbitMQ

Best regards!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions