[
https://issues.apache.org/jira/browse/CAMEL-25660?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Andrea Cosentino updated CAMEL-25660:
-------------------------------------
Description:
h3. Summary
With {{enableDeadLettering=true}} in PEEK_LOCK mode,
{{ServiceBusConsumer.ConsumerOnCompletion.onFailure}} dead-letters the message
with a reason and a description built from the route's exception. Two problems:
# The call is not guarded, so a rejected dead-letter request escapes
{{onFailure}} and the message is never dead-lettered at all.
# Neither field is bounded, so Camel does nothing to keep the message under the
size quota that Azure's own documentation warns about for exactly this practice.
h3. The unguarded call
{code:java}
try {
...
messageContext.deadLetter(deadLetterOptions);
...
} finally {
pendingExchanges.decrementAndGet();
}
{code}
There is no {{catch}}. If the broker rejects the request, the exception leaves
{{onFailure}} into the Azure SDK's processor callback, and nothing reports it
through the consumer's configured {{ExceptionHandler}}. The message stays
locked, the lock expires, Service Bus redelivers it, the route fails the same
way - and this repeats until {{maxDeliveryCount}} is reached, at which point
the broker dead-letters it itself with reason {{MaxDeliveryCountExceeded}}. The
outcome is that the diagnostics {{enableDeadLettering}} exists to provide are
exactly what gets lost, and the exchange is processed several more times than
intended.
{{onComplete}} has the same unguarded shape around
{{messageContext.complete()}}, which is worth deciding on at the same time.
h3. The unbounded fields
{code:java}
deadLetterOptions.setDeadLetterReason(
String.format("%s: %s", cause.getClass().getName(),
cause.getMessage()));
deadLetterOptions.setDeadLetterErrorDescription(Arrays.stream(cause.getStackTrace())
.map(StackTraceElement::toString)
.collect(Collectors.joining("\n")));
{code}
To be clear, the *content* here is what Azure asks for. The dead-letter queue
documentation says:
{quote}We recommend that you include the type of the exception in the
{{DeadLetterReason}} and the stack trace of the exception in the
{{DeadLetterDescription}} as it makes it easier to troubleshoot the cause of
the problem resulting in messages being dead-lettered. It might result in some
messages exceeding the 256 KB quota limit for the Standard tier of Azure
Service Bus.{quote}
So the component follows the recommendation but not the caveat: a full Camel
stack trace, with error-handler and processor frames, is routinely several
thousand characters, and {{cause.getMessage()}} is itself unbounded (a database
error quoting the statement, a validation report). Nothing truncates either
one, and {{com.azure.messaging.servicebus.models.DeadLetterOptions}} does no
client-side length validation (checked against azure-messaging-servicebus
7.17.16), so the broker is what rejects it - landing on problem 1 above.
h3. Suggested fix
Bound both fields, keeping the recommended content but capping it, and guard
the dead-letter call so a rejection is reported through the exception handler
rather than escaping {{onFailure}}. The head of a stack trace is the useful
part; the tail is error-handler frames.
h3. Evidence
* Verified by reading the component on main: no {{catch}} around
{{deadLetter}}, neither field bounded.
* Verified that {{DeadLetterOptions}} has no length validation in the SDK.
* The 256 KB Standard-tier caveat is quoted from Microsoft's dead-letter queue
documentation above.
* *Not* reproduced against a live namespace. The exact payload size at which
the broker rejects the request has not been measured, and the documentation
gives a message-size quota rather than a per-field limit.
h4. Correction to the original description
The first version of this issue claimed a documented 4096 limit on the
dead-letter reason and description. That came from the .NET
{{DeadLetterMessageAsync}} API reference and is *not* corroborated by the
dead-letter queue page or the Service Bus quotas page, so I am retracting it.
The documented constraint is the 256 KB message-size quota on the Standard
tier, quoted above. The unguarded call - the more serious half - is unaffected
either way, since it is verified in Camel's own code.
was:
h3. Summary
With {{enableDeadLettering=true}} in PEEK_LOCK mode,
{{ServiceBusConsumer.onFailure}} fills both dead-letter fields straight from
the route's exception, with no length limit:
{code:java}
deadLetterOptions.setDeadLetterReason(
String.format("%s: %s", cause.getClass().getName(),
cause.getMessage()));
deadLetterOptions.setDeadLetterErrorDescription(Arrays.stream(cause.getStackTrace())
.map(StackTraceElement::toString)
.collect(Collectors.joining("\n")));
messageContext.deadLetter(deadLetterOptions);
{code}
A full Camel stack trace, with the error-handler and processor frames, is
routinely several thousand characters. Microsoft's reference for
{{ServiceBusReceiver.DeadLetterMessageAsync}} lists 4096 as the ceiling for the
dead-letter reason or the error description, and exceeding it is one of the
documented error conditions for the call. So for an ordinary route failure the
description can be over the limit.
h3. Why it matters
The call is in a {{try}} whose only companion is a {{finally}} that decrements
{{pendingExchanges}} - there is no {{catch}}:
{code:java}
try {
...
messageContext.deadLetter(deadLetterOptions);
...
} finally {
pendingExchanges.decrementAndGet();
}
{code}
So if the broker rejects the dead-letter request, the exception leaves
{{onFailure}} and the message is *not* dead-lettered. Its lock then expires and
Service Bus redelivers it, the route fails the same way, and the same oversized
description is sent again - until {{maxDeliveryCount}} is reached and the
broker dead-letters it on its own terms. The net effect is that
{{enableDeadLettering}} does not deliver the diagnostics it is there for, and
the exchange is processed several more times than intended.
The reason field has the same shape: {{cause.getMessage()}} is unbounded, and
some messages are long (a database error quoting the statement, a validation
report).
h3. Suggested fix
Truncate both fields to what Service Bus accepts before calling {{deadLetter}},
and wrap the call so that a broker rejection is reported through the exception
handler instead of escaping {{onFailure}}. Keeping the head of the stack trace
is the useful part; the tail is error-handler frames.
h3. Evidence and what is NOT verified
* Verified by reading the component on main: both fields are unbounded, and
there is no {{catch}} around {{deadLetter}}.
* Verified that {{com.azure.messaging.servicebus.models.DeadLetterOptions}}
(azure-messaging-servicebus 7.17.16) does no client-side length validation, so
the broker is what rejects it.
* *Not* verified against a live namespace - this has not been reproduced, and
the 4096 figure comes from the SDK reference rather than the Service Bus quotas
page, which also leaves it unclear whether the limit counts characters or
bytes. Someone with a Service Bus namespace should confirm the threshold and
the resulting behaviour before the fix is sized.
Filed unassigned.
Found while sweeping for consumers that echo exception detail. Unlike that
class, this is not an information-disclosure concern: the text goes to the
operator's own dead-letter queue, not to an untrusted sender.
> camel-azure-servicebus - dead-lettering builds unbounded reason and
> description from the exception, and a failure to dead-letter is not caught
> ----------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-25660
> URL: https://issues.apache.org/jira/browse/CAMEL-25660
> Project: Camel
> Issue Type: Bug
> Components: camel-azure-servicebus
> Reporter: Andrea Cosentino
> Priority: Major
>
> h3. Summary
> With {{enableDeadLettering=true}} in PEEK_LOCK mode,
> {{ServiceBusConsumer.ConsumerOnCompletion.onFailure}} dead-letters the
> message with a reason and a description built from the route's exception. Two
> problems:
> # The call is not guarded, so a rejected dead-letter request escapes
> {{onFailure}} and the message is never dead-lettered at all.
> # Neither field is bounded, so Camel does nothing to keep the message under
> the size quota that Azure's own documentation warns about for exactly this
> practice.
> h3. The unguarded call
> {code:java}
> try {
> ...
> messageContext.deadLetter(deadLetterOptions);
> ...
> } finally {
> pendingExchanges.decrementAndGet();
> }
> {code}
> There is no {{catch}}. If the broker rejects the request, the exception
> leaves {{onFailure}} into the Azure SDK's processor callback, and nothing
> reports it through the consumer's configured {{ExceptionHandler}}. The
> message stays locked, the lock expires, Service Bus redelivers it, the route
> fails the same way - and this repeats until {{maxDeliveryCount}} is reached,
> at which point the broker dead-letters it itself with reason
> {{MaxDeliveryCountExceeded}}. The outcome is that the diagnostics
> {{enableDeadLettering}} exists to provide are exactly what gets lost, and the
> exchange is processed several more times than intended.
> {{onComplete}} has the same unguarded shape around
> {{messageContext.complete()}}, which is worth deciding on at the same time.
> h3. The unbounded fields
> {code:java}
> deadLetterOptions.setDeadLetterReason(
> String.format("%s: %s", cause.getClass().getName(),
> cause.getMessage()));
> deadLetterOptions.setDeadLetterErrorDescription(Arrays.stream(cause.getStackTrace())
> .map(StackTraceElement::toString)
> .collect(Collectors.joining("\n")));
> {code}
> To be clear, the *content* here is what Azure asks for. The dead-letter queue
> documentation says:
> {quote}We recommend that you include the type of the exception in the
> {{DeadLetterReason}} and the stack trace of the exception in the
> {{DeadLetterDescription}} as it makes it easier to troubleshoot the cause of
> the problem resulting in messages being dead-lettered. It might result in
> some messages exceeding the 256 KB quota limit for the Standard tier of Azure
> Service Bus.{quote}
> So the component follows the recommendation but not the caveat: a full Camel
> stack trace, with error-handler and processor frames, is routinely several
> thousand characters, and {{cause.getMessage()}} is itself unbounded (a
> database error quoting the statement, a validation report). Nothing truncates
> either one, and {{com.azure.messaging.servicebus.models.DeadLetterOptions}}
> does no client-side length validation (checked against
> azure-messaging-servicebus 7.17.16), so the broker is what rejects it -
> landing on problem 1 above.
> h3. Suggested fix
> Bound both fields, keeping the recommended content but capping it, and guard
> the dead-letter call so a rejection is reported through the exception handler
> rather than escaping {{onFailure}}. The head of a stack trace is the useful
> part; the tail is error-handler frames.
> h3. Evidence
> * Verified by reading the component on main: no {{catch}} around
> {{deadLetter}}, neither field bounded.
> * Verified that {{DeadLetterOptions}} has no length validation in the SDK.
> * The 256 KB Standard-tier caveat is quoted from Microsoft's dead-letter
> queue documentation above.
> * *Not* reproduced against a live namespace. The exact payload size at which
> the broker rejects the request has not been measured, and the documentation
> gives a message-size quota rather than a per-field limit.
> h4. Correction to the original description
> The first version of this issue claimed a documented 4096 limit on the
> dead-letter reason and description. That came from the .NET
> {{DeadLetterMessageAsync}} API reference and is *not* corroborated by the
> dead-letter queue page or the Service Bus quotas page, so I am retracting it.
> The documented constraint is the 256 KB message-size quota on the Standard
> tier, quoted above. The unguarded call - the more serious half - is
> unaffected either way, since it is verified in Camel's own code.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)