114114import java .util .Map ;
115115import java .util .Set ;
116116import java .util .concurrent .TimeoutException ;
117+ import java .util .concurrent .atomic .AtomicReference ;
117118import java .util .function .BiFunction ;
118119import java .util .function .Consumer ;
119120
@@ -1318,6 +1319,18 @@ Mono<BlobDownloadAsyncResponse> downloadStreamWithResponse(BlobRange range, Down
13181319 finalCount = finalRange .getCount ();
13191320 }
13201321
1322+ AtomicReference <StorageContentValidationDecoderPolicy .DecoderState > decoderStateRef
1323+ = new AtomicReference <>();
1324+ if (contentValidationOptions != null
1325+ && contentValidationOptions .isStructuredMessageValidationEnabled ()) {
1326+ Object decoderStateObj
1327+ = firstRangeContext .getData (Constants .STRUCTURED_MESSAGE_DECODER_STATE_CONTEXT_KEY )
1328+ .orElse (null );
1329+ if (decoderStateObj instanceof StorageContentValidationDecoderPolicy .DecoderState ) {
1330+ decoderStateRef .set ((StorageContentValidationDecoderPolicy .DecoderState ) decoderStateObj );
1331+ }
1332+ }
1333+
13211334 // The resume function takes throwable and offset at the destination.
13221335 // I.e. offset is relative to the starting point.
13231336 BiFunction <Throwable , Long , Mono <StreamResponse >> onDownloadErrorResume = (throwable , offset ) -> {
@@ -1326,18 +1339,28 @@ Mono<BlobDownloadAsyncResponse> downloadStreamWithResponse(BlobRange range, Down
13261339 }
13271340
13281341 long newCount = finalCount - offset ;
1342+ StorageContentValidationDecoderPolicy .DecoderState decoderState = null ;
1343+ long expectedEncodedLength = finalCount ;
1344+ long encodedProgress = offset ;
1345+
1346+ if (contentValidationOptions != null
1347+ && contentValidationOptions .isStructuredMessageValidationEnabled ()) {
1348+ decoderState = decoderStateRef .get ();
1349+
1350+ if (decoderState == null ) {
1351+ Object decoderStateObj
1352+ = firstRangeContext .getData (Constants .STRUCTURED_MESSAGE_DECODER_STATE_CONTEXT_KEY )
1353+ .orElse (null );
13291354
1330- /*
1331- * It's possible that the network stream will throw an error after emitting all data but before
1332- * completing. Issuing a retry at this stage would leave the download in a bad state with
1333- * incorrect count and offset values. Because we have read the intended amount of data, we can
1334- * ignore the error at the end of the stream.
1335- */
1336- if (newCount == 0 ) {
1337- LOGGER .warning ("Exception encountered in ReliableDownload after all data read from the network "
1338- + "but before stream signaled completion. Returning success as all data was downloaded. "
1339- + "Exception message: " + throwable .getMessage ());
1340- return Mono .empty ();
1355+ if (decoderStateObj instanceof StorageContentValidationDecoderPolicy .DecoderState ) {
1356+ decoderState = (StorageContentValidationDecoderPolicy .DecoderState ) decoderStateObj ;
1357+ }
1358+ }
1359+
1360+ if (decoderState != null ) {
1361+ expectedEncodedLength = decoderState .getExpectedContentLength ();
1362+ encodedProgress = decoderState .getTotalEncodedBytesProcessed ();
1363+ }
13411364 }
13421365
13431366 try {
@@ -1349,53 +1372,38 @@ Mono<BlobDownloadAsyncResponse> downloadStreamWithResponse(BlobRange range, Down
13491372 // based on the encoded bytes processed, not the decoded bytes
13501373 if (contentValidationOptions != null
13511374 && contentValidationOptions .isStructuredMessageValidationEnabled ()) {
1352- // Get the decoder state to determine how many encoded bytes were processed
1353- Object decoderStateObj
1354- = firstRangeContext .getData (Constants .STRUCTURED_MESSAGE_DECODER_STATE_CONTEXT_KEY )
1355- .orElse (null );
1356-
1357- if (decoderStateObj instanceof StorageContentValidationDecoderPolicy .DecoderState ) {
1358- StorageContentValidationDecoderPolicy .DecoderState decoderState
1359- = (StorageContentValidationDecoderPolicy .DecoderState ) decoderStateObj ;
1375+ long retryStartOffset = -1 ;
13601376
1361- // Use getRetryOffset() to get the correct offset for retry
1362- // This accounts for pending bytes that have been received but not yet consumed
1363- long encodedOffset = decoderState .getRetryOffset ();
1364- long remainingCount = finalCount - encodedOffset ;
1365- retryRange = new BlobRange (initialOffset + encodedOffset , remainingCount );
1377+ // First try to use decoder state (authoritative)
1378+ if (decoderState != null ) {
1379+ // Always rewind decoder to last validated boundary before retrying.
1380+ retryStartOffset = decoderState .resetForRetry ();
13661381
1367- LOGGER .info (
1368- "Structured message smart retry: resuming from offset {} (initial={}, encoded={})" ,
1369- initialOffset + encodedOffset , initialOffset , encodedOffset );
1370-
1371- // Preserve the decoder state for the retry
13721382 retryContext = retryContext
13731383 .addData (Constants .STRUCTURED_MESSAGE_DECODER_STATE_CONTEXT_KEY , decoderState );
1374- } else {
1375- // No decoder state available, try to parse retry offset from exception message
1376- // The exception message contains RETRY-START-OFFSET=<number> token
1377- long retryStartOffset = StorageContentValidationDecoderPolicy
1384+ decoderStateRef .set (decoderState );
1385+ }
1386+
1387+ // If no decoder state or no retry offset from state, fall back to parsed token or offset.
1388+ if (retryStartOffset < 0 ) {
1389+ retryStartOffset = StorageContentValidationDecoderPolicy
13781390 .parseRetryStartOffset (throwable .getMessage ());
1379- if (retryStartOffset >= 0 ) {
1380- long remainingCount = finalCount - retryStartOffset ;
1381- // Validate remainingCount to avoid negative values
1382- if (remainingCount <= 0 ) {
1383- LOGGER .warning ("Retry offset {} exceeds finalCount {}, using fallback" ,
1384- retryStartOffset , finalCount );
1385- retryRange = new BlobRange (initialOffset + offset , newCount );
1386- } else {
1387- retryRange = new BlobRange (initialOffset + retryStartOffset , remainingCount );
1388-
1389- LOGGER .info (
1390- "Structured message smart retry from exception: resuming from offset {} "
1391- + "(initial={}, parsed={})" ,
1392- initialOffset + retryStartOffset , initialOffset , retryStartOffset );
1393- }
1394- } else {
1395- // Fallback to normal retry logic if no offset found
1396- retryRange = new BlobRange (initialOffset + offset , newCount );
1397- }
13981391 }
1392+ if (retryStartOffset < 0 ) {
1393+ retryStartOffset = offset ;
1394+ }
1395+
1396+ long remainingCount = expectedEncodedLength - retryStartOffset ;
1397+ if (remainingCount < 0 ) {
1398+ remainingCount = expectedEncodedLength - offset ;
1399+ retryStartOffset = offset ;
1400+ }
1401+
1402+ retryRange = new BlobRange (initialOffset + retryStartOffset , remainingCount );
1403+
1404+ LOGGER .info (
1405+ "Structured message smart retry: resuming from offset {} (initial={}, encoded={}, remaining={})" ,
1406+ initialOffset + retryStartOffset , initialOffset , retryStartOffset , remainingCount );
13991407 } else {
14001408 // For non-structured downloads, use smart retry from the interrupted offset
14011409 retryRange = new BlobRange (initialOffset + offset , newCount );
@@ -1410,6 +1418,7 @@ Mono<BlobDownloadAsyncResponse> downloadStreamWithResponse(BlobRange range, Down
14101418 // Structured message decoding is now handled by StructuredMessageDecoderPolicy
14111419 return BlobDownloadAsyncResponseConstructorProxy .create (response , onDownloadErrorResume , finalOptions );
14121420 });
1421+
14131422 }
14141423
14151424 Mono <BlobDownloadAsyncResponse > downloadStreamWithResponse (BlobRange range , DownloadRetryOptions options ,
0 commit comments