@@ -449,6 +449,48 @@ void unknownContentLength_multiChunk_allCallbacksFire() {
449449 Mockito .verify (transferListenerMock , atLeastOnce ()).bytesTransferred (ArgumentMatchers .any ());
450450 }
451451
452+ /**
453+ * Verifies that when an unknown-content-length upload fails on the single-chunk path,
454+ * the completionFuture completes exceptionally and transferFailed fires.
455+ * This guards against regressions where the failure path in uploadInOneChunk does not
456+ * propagate the exception to returnFuture, causing the upload to hang indefinitely.
457+ */
458+ @ Test
459+ void unknownContentLength_singleChunk_failurePropagates () {
460+ S3AsyncClient s3Async = s3AsyncClient (true );
461+
462+ stubFor (put (urlPathEqualTo ("/" + EXAMPLE_BUCKET + "/" + TEST_KEY ))
463+ .willReturn (aResponse ().withStatus (500 ).withBody (ERROR_BODY )));
464+
465+ S3TransferManager tm = new GenericS3TransferManager (s3Async , mock (UploadDirectoryHelper .class ),
466+ mock (TransferManagerConfiguration .class ),
467+ mock (DownloadDirectoryHelper .class ));
468+ CaptureTransferListener transferListener = new CaptureTransferListener ();
469+ TransferListener transferListenerMock = mock (TransferListener .class );
470+
471+ BlockingInputStreamAsyncRequestBody body = AsyncRequestBody .forBlockingInputStream (null );
472+
473+ Upload upload = tm .upload (u -> u .putObjectRequest (p -> p .bucket (EXAMPLE_BUCKET ).key (TEST_KEY ))
474+ .requestBody (body )
475+ .addTransferListener (transferListener )
476+ .addTransferListener (transferListenerMock )
477+ .build ());
478+
479+ byte [] data = new byte [1024 ];
480+ body .writeInputStream (new ByteArrayInputStream (data ));
481+
482+ assertThatExceptionOfType (CompletionException .class ).isThrownBy (() -> upload .completionFuture ().join ());
483+
484+ assertTransferListenerCompletion (transferListener );
485+ assertThat (transferListener .isTransferInitiated ()).isTrue ();
486+ assertThat (transferListener .isTransferComplete ()).isFalse ();
487+ assertThat (transferListener .getExceptionCaught ()).isNotNull ();
488+
489+ Mockito .verify (transferListenerMock , times (1 )).transferInitiated (ArgumentMatchers .any ());
490+ Mockito .verify (transferListenerMock , times (0 )).transferComplete (ArgumentMatchers .any ());
491+ Mockito .verify (transferListenerMock , timeout (1000 ).times (1 )).transferFailed (ArgumentMatchers .any ());
492+ }
493+
452494 private static void assertTransferListenerCompletion (CaptureTransferListener transferListener ) {
453495 Duration waitDuration = Duration .ofSeconds (5 );
454496 assertTimeoutPreemptively (
0 commit comments