@@ -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
@@ -1395,6 +1399,167 @@ void testProgressListener_orderingUnderConcurrency_pinsSequentialExecutor() thro
13951399 }
13961400 }
13971401
1402+ @ Test
1403+ void testActionableErrors_startFailure_preservesOriginalExceptionWithoutEndpointSuffix () {
1404+ ApiException startError = createApiException (401 , StatusCode .Code .UNAUTHENTICATED );
1405+ when (mockStartCallable .futureCall (any (), any ()))
1406+ .thenReturn (ApiFutures .immediateFailedFuture (startError ));
1407+
1408+ ResumableUploadFuture <String > future =
1409+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1410+
1411+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1412+ assertThat (ex .getCause ()).isSameInstanceAs (startError );
1413+ assertThat (ex .getCause ().getMessage ()).doesNotContain ("endpoint:" );
1414+ assertThat (future .getUploadSessionUrl ()).isNull ();
1415+ }
1416+
1417+ @ Test
1418+ void testActionableErrors_chunkFailure_messageContainsUploadSessionUrl () {
1419+ String sessionUrl = "https://upload.url/chunk-error-test" ;
1420+ stubStartSession (sessionUrl );
1421+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1422+ .thenReturn (
1423+ ApiFutures .immediateFailedFuture (
1424+ createApiException (403 , StatusCode .Code .PERMISSION_DENIED )));
1425+
1426+ ResumableUploadFuture <String > future =
1427+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1428+
1429+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1430+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1431+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1432+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1433+ }
1434+
1435+ @ Test
1436+ void testActionableErrors_preservesErrorDetailsCauseChainAndSuppressedExceptions () {
1437+ String sessionUrl = "https://upload.url/chunk-error-details-test" ;
1438+ stubStartSession (sessionUrl );
1439+ ErrorDetails errorDetails = ErrorDetails .builder ().build ();
1440+ ApiException original =
1441+ ApiExceptionFactory .createException (
1442+ "HTTP 403" ,
1443+ null ,
1444+ new HttpStatusStatusCode (403 , StatusCode .Code .PERMISSION_DENIED ),
1445+ false ,
1446+ errorDetails );
1447+ IOException suppressed = new IOException ("underlying stream error" );
1448+ original .addSuppressed (suppressed );
1449+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1450+ .thenReturn (ApiFutures .immediateFailedFuture (original ));
1451+
1452+ ResumableUploadFuture <String > future =
1453+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1454+
1455+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1456+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1457+ ApiException cause = (ApiException ) ex .getCause ();
1458+ assertThat (cause .getMessage ()).contains (sessionUrl );
1459+ assertThat (cause .getCause ()).isSameInstanceAs (original );
1460+ assertThat (cause .getErrorDetails ()).isSameInstanceAs (errorDetails );
1461+ assertThat (cause .getSuppressed ()).asList ().contains (suppressed );
1462+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1463+ }
1464+
1465+ @ Test
1466+ void testActionableErrors_recoveryFailure_messageContainsUploadSessionUrl () {
1467+ String sessionUrl = "https://upload.url/recovery-error-test" ;
1468+ stubStartSession (sessionUrl );
1469+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1470+ .thenReturn (
1471+ ApiFutures .immediateFailedFuture (
1472+ createApiException (400 , StatusCode .Code .INVALID_ARGUMENT )));
1473+ when (mockQueryCallable .futureCall (any (QueryStatusRequest .class ), any ()))
1474+ .thenReturn (
1475+ ApiFutures .immediateFailedFuture (
1476+ createApiException (403 , StatusCode .Code .PERMISSION_DENIED )));
1477+
1478+ ResumableUploadFuture <String > future =
1479+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1480+
1481+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1482+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1483+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1484+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1485+ }
1486+
1487+ @ Test
1488+ void testActionableErrors_globalTimeoutFailure_messageContainsUploadSessionUrl () {
1489+ String sessionUrl = "https://upload.url/timeout-error-test" ;
1490+ stubStartSession (sessionUrl );
1491+ SettableApiFuture <ChunkUploadResponse <String >> hungChunk = SettableApiFuture .create ();
1492+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ())).thenReturn (hungChunk );
1493+
1494+ ResumableUploadCallSettings settings =
1495+ defaultSettings .toBuilder ().setGlobalTimeout (Duration .ofMillis (50 )).build ();
1496+
1497+ ResumableUploadFuture <String > future =
1498+ callable .futureCall ("resource-path" , streamOf ("hello" ), null , settings );
1499+
1500+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1501+ assertThat (ex .getCause ()).isInstanceOf (DeadlineExceededException .class );
1502+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1503+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1504+ }
1505+
1506+ @ Test
1507+ void testActionableErrors_rewindFailure_surfacesActionableSeekableStreamMessage () {
1508+ String sessionUrl = "https://upload.url/rewind-error-test" ;
1509+ stubStartSession (sessionUrl );
1510+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1511+ .thenReturn (
1512+ ApiFutures .immediateFuture (
1513+ ChunkUploadResponse .create (ResumableUploadStatus .ACTIVE , null )))
1514+ .thenReturn (
1515+ ApiFutures .immediateFailedFuture (
1516+ createApiException (400 , StatusCode .Code .INVALID_ARGUMENT )));
1517+
1518+ when (mockQueryCallable .futureCall (any (QueryStatusRequest .class ), any ()))
1519+ .thenReturn (
1520+ ApiFutures .immediateFuture (
1521+ createQueryResponse (false , 4L , null , ResumableUploadStatus .ACTIVE )));
1522+
1523+ byte [] data = new byte [16 ];
1524+ ResumableUploadFuture <String > future =
1525+ callable .futureCall ("resource-path" , new ByteArrayInputStream (data ), null );
1526+
1527+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1528+ assertThat (ex .getCause ()).isInstanceOf (FailedPreconditionException .class );
1529+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1530+ assertThat (ex .getCause ().getMessage ()).contains ("seekable stream" );
1531+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1532+ }
1533+
1534+ @ Test
1535+ void testUploadCallable_failureOutcome_attachesCloseExceptionViaAddSuppressed () {
1536+ when (mockStartCallable .futureCall (any (), any ()))
1537+ .thenReturn (ApiFutures .immediateFailedFuture (new IllegalStateException ("upload failed" )));
1538+
1539+ InputStream failingStream =
1540+ new InputStream () {
1541+ @ Override
1542+ public int read () {
1543+ return -1 ;
1544+ }
1545+
1546+ @ Override
1547+ public void close () throws IOException {
1548+ throw new IOException ("stream close error" );
1549+ }
1550+ };
1551+
1552+ ResumableUploadFuture <String > future =
1553+ callable .futureCall ("resource-path" , failingStream , null );
1554+ ExecutionException exception = assertThrows (ExecutionException .class , future ::get );
1555+ assertThat (exception .getCause ()).isInstanceOf (IllegalStateException .class );
1556+ assertThat (exception .getCause ().getSuppressed ()).asList ().hasSize (1 );
1557+ assertThat (exception .getCause ().getSuppressed ()[0 ]).isInstanceOf (IOException .class );
1558+ assertThat (exception .getCause ().getSuppressed ()[0 ])
1559+ .hasMessageThat ()
1560+ .contains ("stream close error" );
1561+ }
1562+
13981563 private static class HttpStatusStatusCode implements StatusCode {
13991564 private final int httpStatus ;
14001565 private final StatusCode .Code code ;
0 commit comments