@@ -110,7 +110,11 @@ void setUp() {
110110 callContext = FakeCallContext .createDefault ();
111111 executor = Executors .newScheduledThreadPool (2 );
112112 clientContext =
113- ClientContext .newBuilder ().setDefaultCallContext (callContext ).setExecutor (executor ).build ();
113+ ClientContext .newBuilder ()
114+ .setDefaultCallContext (callContext )
115+ .setExecutor (executor )
116+ .setEndpoint ("https://test.endpoint.com" )
117+ .build ();
114118 callable = new ResumableUploadCallableImpl <>(mockClient , defaultSettings , clientContext );
115119 }
116120
@@ -1331,6 +1335,168 @@ void testProgressListener_orderingUnderConcurrency_pinsSequentialExecutor() thro
13311335 }
13321336 }
13331337
1338+ @ Test
1339+ void testActionableErrors_startFailure_preservesOriginalExceptionWithoutEndpointSuffix () {
1340+ ApiException startError = createApiException (401 , StatusCode .Code .UNAUTHENTICATED );
1341+ when (mockStartCallable .futureCall (any (), any ()))
1342+ .thenReturn (ApiFutures .immediateFailedFuture (startError ));
1343+
1344+ ResumableUploadFuture <String > future =
1345+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1346+
1347+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1348+ assertThat (ex .getCause ()).isSameInstanceAs (startError );
1349+ assertThat (ex .getCause ().getMessage ()).doesNotContain ("endpoint:" );
1350+ assertThat (future .getUploadSessionUrl ()).isNull ();
1351+ }
1352+
1353+ @ Test
1354+ void testActionableErrors_chunkFailure_messageContainsUploadSessionUrl () {
1355+ String sessionUrl = "https://upload.url/chunk-error-test" ;
1356+ stubStartSession (sessionUrl );
1357+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1358+ .thenReturn (
1359+ ApiFutures .immediateFailedFuture (
1360+ createApiException (403 , StatusCode .Code .PERMISSION_DENIED )));
1361+
1362+ ResumableUploadFuture <String > future =
1363+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1364+
1365+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1366+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1367+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1368+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1369+ }
1370+
1371+ @ Test
1372+ void testActionableErrors_preservesErrorDetailsCauseChainAndSuppressedExceptions () {
1373+ String sessionUrl = "https://upload.url/chunk-error-details-test" ;
1374+ stubStartSession (sessionUrl );
1375+ ErrorDetails errorDetails = ErrorDetails .builder ().build ();
1376+ ApiException original =
1377+ ApiExceptionFactory .createException (
1378+ "HTTP 403" ,
1379+ null ,
1380+ new HttpStatusStatusCode (403 , StatusCode .Code .PERMISSION_DENIED ),
1381+ false ,
1382+ errorDetails );
1383+ IOException suppressed = new IOException ("underlying stream error" );
1384+ original .addSuppressed (suppressed );
1385+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1386+ .thenReturn (ApiFutures .immediateFailedFuture (original ));
1387+
1388+ ResumableUploadFuture <String > future =
1389+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1390+
1391+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1392+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1393+ ApiException cause = (ApiException ) ex .getCause ();
1394+ assertThat (cause .getMessage ()).contains (sessionUrl );
1395+ assertThat (cause .getCause ()).isSameInstanceAs (original );
1396+ assertThat (cause .getErrorDetails ()).isSameInstanceAs (errorDetails );
1397+ assertThat (cause .getSuppressed ()).asList ().contains (suppressed );
1398+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1399+ }
1400+
1401+ @ Test
1402+ void testActionableErrors_recoveryFailure_messageContainsUploadSessionUrl () {
1403+ String sessionUrl = "https://upload.url/recovery-error-test" ;
1404+ stubStartSession (sessionUrl );
1405+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1406+ .thenReturn (
1407+ ApiFutures .immediateFailedFuture (
1408+ createApiException (400 , StatusCode .Code .INVALID_ARGUMENT )));
1409+ when (mockQueryCallable .futureCall (any (QueryStatusRequest .class ), any ()))
1410+ .thenReturn (
1411+ ApiFutures .immediateFailedFuture (
1412+ createApiException (403 , StatusCode .Code .PERMISSION_DENIED )));
1413+
1414+ ResumableUploadFuture <String > future =
1415+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1416+
1417+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1418+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1419+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1420+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1421+ }
1422+
1423+ @ Test
1424+ void testActionableErrors_globalTimeoutFailure_messageContainsUploadSessionUrl () {
1425+ String sessionUrl = "https://upload.url/timeout-error-test" ;
1426+ stubStartSession (sessionUrl );
1427+ SettableApiFuture <ChunkUploadResponse <String >> hungChunk = SettableApiFuture .create ();
1428+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ())).thenReturn (hungChunk );
1429+
1430+ ResumableUploadCallSettings settings =
1431+ defaultSettings .toBuilder ().setGlobalTimeout (Duration .ofMillis (50 )).build ();
1432+
1433+ ResumableUploadFuture <String > future =
1434+ callable .futureCall ("resource-path" , streamOf ("hello" ), null , settings );
1435+
1436+ ExecutionException ex =
1437+ assertThrows (ExecutionException .class , () -> future .get (5 , TimeUnit .SECONDS ));
1438+ assertThat (ex .getCause ()).isInstanceOf (DeadlineExceededException .class );
1439+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1440+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1441+ }
1442+
1443+ @ Test
1444+ void testActionableErrors_rewindFailure_surfacesActionableSeekableStreamMessage () {
1445+ String sessionUrl = "https://upload.url/rewind-error-test" ;
1446+ stubStartSession (sessionUrl );
1447+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1448+ .thenReturn (
1449+ ApiFutures .immediateFuture (
1450+ ChunkUploadResponse .create (ResumableUploadStatus .ACTIVE , null )))
1451+ .thenReturn (
1452+ ApiFutures .immediateFailedFuture (
1453+ createApiException (400 , StatusCode .Code .INVALID_ARGUMENT )));
1454+
1455+ when (mockQueryCallable .futureCall (any (QueryStatusRequest .class ), any ()))
1456+ .thenReturn (
1457+ ApiFutures .immediateFuture (
1458+ createQueryResponse (4L , null , ResumableUploadStatus .ACTIVE )));
1459+
1460+ byte [] data = new byte [16 ];
1461+ ResumableUploadFuture <String > future =
1462+ callable .futureCall ("resource-path" , new ByteArrayInputStream (data ), null );
1463+
1464+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1465+ assertThat (ex .getCause ()).isInstanceOf (FailedPreconditionException .class );
1466+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1467+ assertThat (ex .getCause ().getMessage ()).contains ("seekable stream" );
1468+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1469+ }
1470+
1471+ @ Test
1472+ void testUploadCallable_failureOutcome_attachesCloseExceptionViaAddSuppressed () {
1473+ when (mockStartCallable .futureCall (any (), any ()))
1474+ .thenReturn (ApiFutures .immediateFailedFuture (new IllegalStateException ("upload failed" )));
1475+
1476+ InputStream failingStream =
1477+ new InputStream () {
1478+ @ Override
1479+ public int read () {
1480+ return -1 ;
1481+ }
1482+
1483+ @ Override
1484+ public void close () throws IOException {
1485+ throw new IOException ("stream close error" );
1486+ }
1487+ };
1488+
1489+ ResumableUploadFuture <String > future =
1490+ callable .futureCall ("resource-path" , failingStream , null );
1491+ ExecutionException exception = assertThrows (ExecutionException .class , future ::get );
1492+ assertThat (exception .getCause ()).isInstanceOf (IllegalStateException .class );
1493+ assertThat (exception .getCause ().getSuppressed ()).asList ().hasSize (1 );
1494+ assertThat (exception .getCause ().getSuppressed ()[0 ]).isInstanceOf (IOException .class );
1495+ assertThat (exception .getCause ().getSuppressed ()[0 ])
1496+ .hasMessageThat ()
1497+ .contains ("stream close error" );
1498+ }
1499+
13341500 private static class HttpStatusStatusCode implements StatusCode {
13351501 private final int httpStatus ;
13361502 private final StatusCode .Code code ;
0 commit comments