@@ -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
@@ -1471,6 +1475,138 @@ void testProgressListener_orderingUnderConcurrency_pinsSequentialExecutor() thro
14711475 }
14721476 }
14731477
1478+ @ Test
1479+ void testActionableErrors_startFailure_preservesOriginalExceptionWithoutEndpointSuffix () {
1480+ ApiException startError = createApiException (401 , StatusCode .Code .UNAUTHENTICATED );
1481+ when (mockStartCallable .futureCall (any (), any ()))
1482+ .thenReturn (ApiFutures .immediateFailedFuture (startError ));
1483+
1484+ ResumableUploadFuture <String > future =
1485+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1486+
1487+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1488+ assertThat (ex .getCause ()).isSameInstanceAs (startError );
1489+ assertThat (ex .getCause ().getMessage ()).doesNotContain ("endpoint:" );
1490+ assertThat (future .getUploadSessionUrl ()).isNull ();
1491+ }
1492+
1493+ @ Test
1494+ void testActionableErrors_chunkFailure_messageContainsUploadSessionUrl () {
1495+ String sessionUrl = "https://upload.url/chunk-error-test" ;
1496+ stubStartSession (sessionUrl );
1497+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1498+ .thenReturn (
1499+ ApiFutures .immediateFailedFuture (
1500+ createApiException (403 , StatusCode .Code .PERMISSION_DENIED )));
1501+
1502+ ResumableUploadFuture <String > future =
1503+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1504+
1505+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1506+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1507+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1508+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1509+ }
1510+
1511+ @ Test
1512+ void testActionableErrors_preservesErrorDetailsCauseChainAndSuppressedExceptions () {
1513+ String sessionUrl = "https://upload.url/chunk-error-details-test" ;
1514+ stubStartSession (sessionUrl );
1515+ ErrorDetails errorDetails = ErrorDetails .builder ().build ();
1516+ ApiException original =
1517+ ApiExceptionFactory .createException (
1518+ "HTTP 403" ,
1519+ null ,
1520+ new HttpStatusStatusCode (403 , StatusCode .Code .PERMISSION_DENIED ),
1521+ false ,
1522+ errorDetails );
1523+ IOException suppressed = new IOException ("underlying stream error" );
1524+ original .addSuppressed (suppressed );
1525+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1526+ .thenReturn (ApiFutures .immediateFailedFuture (original ));
1527+
1528+ ResumableUploadFuture <String > future =
1529+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1530+
1531+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1532+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1533+ ApiException cause = (ApiException ) ex .getCause ();
1534+ assertThat (cause .getMessage ()).contains (sessionUrl );
1535+ assertThat (cause .getCause ()).isSameInstanceAs (original );
1536+ assertThat (cause .getErrorDetails ()).isSameInstanceAs (errorDetails );
1537+ assertThat (cause .getSuppressed ()).asList ().contains (suppressed );
1538+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1539+ }
1540+
1541+ @ Test
1542+ void testActionableErrors_recoveryFailure_messageContainsUploadSessionUrl () {
1543+ String sessionUrl = "https://upload.url/recovery-error-test" ;
1544+ stubStartSession (sessionUrl );
1545+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1546+ .thenReturn (
1547+ ApiFutures .immediateFailedFuture (
1548+ createApiException (400 , StatusCode .Code .INVALID_ARGUMENT )));
1549+ when (mockQueryCallable .futureCall (any (QueryStatusRequest .class ), any ()))
1550+ .thenReturn (
1551+ ApiFutures .immediateFailedFuture (
1552+ createApiException (403 , StatusCode .Code .PERMISSION_DENIED )));
1553+
1554+ ResumableUploadFuture <String > future =
1555+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1556+
1557+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1558+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1559+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1560+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1561+ }
1562+
1563+ @ Test
1564+ void testActionableErrors_globalTimeoutFailure_messageContainsUploadSessionUrl () {
1565+ String sessionUrl = "https://upload.url/timeout-error-test" ;
1566+ stubStartSession (sessionUrl );
1567+ SettableApiFuture <ChunkUploadResponse <String >> hungChunk = SettableApiFuture .create ();
1568+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ())).thenReturn (hungChunk );
1569+
1570+ ResumableUploadCallSettings settings =
1571+ defaultSettings .toBuilder ().setGlobalTimeout (Duration .ofMillis (50 )).build ();
1572+
1573+ ResumableUploadFuture <String > future =
1574+ callable .futureCall ("resource-path" , streamOf ("hello" ), null , settings );
1575+
1576+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1577+ assertThat (ex .getCause ()).isInstanceOf (DeadlineExceededException .class );
1578+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1579+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1580+ }
1581+
1582+ @ Test
1583+ void testActionableErrors_rewindFailure_surfacesActionableSeekableStreamMessage () {
1584+ String sessionUrl = "https://upload.url/rewind-error-test" ;
1585+ stubStartSession (sessionUrl );
1586+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1587+ .thenReturn (
1588+ ApiFutures .immediateFuture (
1589+ ChunkUploadResponse .create (ResumableUploadStatus .ACTIVE , null )))
1590+ .thenReturn (
1591+ ApiFutures .immediateFailedFuture (
1592+ createApiException (400 , StatusCode .Code .INVALID_ARGUMENT )));
1593+
1594+ when (mockQueryCallable .futureCall (any (QueryStatusRequest .class ), any ()))
1595+ .thenReturn (
1596+ ApiFutures .immediateFuture (
1597+ createQueryResponse (false , 4L , null , ResumableUploadStatus .ACTIVE )));
1598+
1599+ byte [] data = new byte [16 ];
1600+ ResumableUploadFuture <String > future =
1601+ callable .futureCall ("resource-path" , new ByteArrayInputStream (data ), null );
1602+
1603+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1604+ assertThat (ex .getCause ()).isInstanceOf (FailedPreconditionException .class );
1605+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1606+ assertThat (ex .getCause ().getMessage ()).contains ("seekable stream" );
1607+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1608+ }
1609+
14741610 private static class HttpStatusStatusCode implements StatusCode {
14751611 private final int httpStatus ;
14761612 private final StatusCode .Code code ;
0 commit comments