@@ -158,7 +158,10 @@ public void OnDequeue(Func<string, Task<bool>> processMessageAction)
158158 {
159159 if ( message . DequeueCount <= MaxRetryBeforePoisonQueue )
160160 {
161- bool success = await processMessageAction . Invoke ( message . MessageText ) . ConfigureAwait ( false ) ;
161+ // Extract the original message text in case this is a re-queued poison message
162+ string messageTextToProcess = ExtractOriginalMessageText ( message . MessageText ) ;
163+
164+ bool success = await processMessageAction . Invoke ( messageTextToProcess ) . ConfigureAwait ( false ) ;
162165 if ( success )
163166 {
164167 this . _log . LogTrace ( "Message '{0}' successfully processed, deleting message" , message . MessageId ) ;
@@ -283,16 +286,26 @@ private async Task DeleteMessageAsync(QueueMessage message, CancellationToken ca
283286
284287 private async Task UnlockMessageAsync ( QueueMessage message , TimeSpan delay , CancellationToken cancellationToken )
285288 {
286- await this . _queue ! . UpdateMessageAsync ( message . MessageId , message . PopReceipt , visibilityTimeout : delay , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
289+ // Pass the original message body to preserve it when updating visibility timeout
290+ // Without passing the message content, Azure may distort the message body
291+ await this . _queue ! . UpdateMessageAsync (
292+ message . MessageId ,
293+ message . PopReceipt ,
294+ message . Body ,
295+ visibilityTimeout : delay ,
296+ cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
287297 }
288298
289299 private async Task MoveMessageToPoisonQueueAsync ( QueueMessage message , CancellationToken cancellationToken )
290300 {
291301 await this . _poisonQueue ! . CreateIfNotExistsAsync ( cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
292302
303+ // Extract the original message text, unwrapping if it was previously wrapped as a poison message
304+ string originalMessageText = ExtractOriginalMessageText ( message . MessageText ) ;
305+
293306 var poisonMsg = new
294307 {
295- MessageText = message . MessageText ,
308+ MessageText = originalMessageText ,
296309 Id = message . MessageId ,
297310 InsertedOn = message . InsertedOn ,
298311 DequeueCount = message . DequeueCount ,
@@ -305,6 +318,40 @@ await this._poisonQueue.SendMessageAsync(
305318 timeToLive : neverExpire , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
306319 await this . DeleteMessageAsync ( message , cancellationToken ) . ConfigureAwait ( false ) ;
307320 }
321+
322+ /// <summary>
323+ /// Extracts the original message text from a potentially wrapped poison message format.
324+ /// This prevents double-wrapping when a message is moved to poison queue multiple times.
325+ /// </summary>
326+ private static string ExtractOriginalMessageText ( string messageText )
327+ {
328+ if ( string . IsNullOrEmpty ( messageText ) )
329+ {
330+ return messageText ;
331+ }
332+
333+ try
334+ {
335+ using var doc = JsonDocument . Parse ( messageText ) ;
336+ var root = doc . RootElement ;
337+
338+ // Check if this is a wrapped poison message format (has MessageText, Id, InsertedOn, DequeueCount)
339+ if ( root . TryGetProperty ( "MessageText" , out var innerMessageText ) &&
340+ root . TryGetProperty ( "Id" , out _ ) &&
341+ root . TryGetProperty ( "DequeueCount" , out _ ) )
342+ {
343+ // This is a wrapped poison message, recursively extract the original
344+ return ExtractOriginalMessageText ( innerMessageText . GetString ( ) ?? messageText ) ;
345+ }
346+ }
347+ catch ( JsonException )
348+ {
349+ // Not a valid JSON, return as-is
350+ }
351+
352+ return messageText ;
353+ }
354+
308355 private static string ToJson ( object data , bool indented = false )
309356 {
310357 return JsonSerializer . Serialize ( data , indented ? s_indentedJsonOptions : s_notIndentedJsonOptions ) ;
0 commit comments