@@ -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
@@ -1504,6 +1508,138 @@ void testProgressListener_orderingUnderConcurrency_pinsSequentialExecutor() thro
15041508 }
15051509 }
15061510
1511+ @ Test
1512+ void testActionableErrors_startFailure_preservesOriginalExceptionWithoutEndpointSuffix () {
1513+ ApiException startError = createApiException (401 , StatusCode .Code .UNAUTHENTICATED );
1514+ when (mockStartCallable .futureCall (any (), any ()))
1515+ .thenReturn (ApiFutures .immediateFailedFuture (startError ));
1516+
1517+ ResumableUploadFuture <String > future =
1518+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1519+
1520+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1521+ assertThat (ex .getCause ()).isSameInstanceAs (startError );
1522+ assertThat (ex .getCause ().getMessage ()).doesNotContain ("endpoint:" );
1523+ assertThat (future .getUploadSessionUrl ()).isNull ();
1524+ }
1525+
1526+ @ Test
1527+ void testActionableErrors_chunkFailure_messageContainsUploadSessionUrl () {
1528+ String sessionUrl = "https://upload.url/chunk-error-test" ;
1529+ stubStartSession (sessionUrl );
1530+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1531+ .thenReturn (
1532+ ApiFutures .immediateFailedFuture (
1533+ createApiException (403 , StatusCode .Code .PERMISSION_DENIED )));
1534+
1535+ ResumableUploadFuture <String > future =
1536+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1537+
1538+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1539+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1540+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1541+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1542+ }
1543+
1544+ @ Test
1545+ void testActionableErrors_preservesErrorDetailsCauseChainAndSuppressedExceptions () {
1546+ String sessionUrl = "https://upload.url/chunk-error-details-test" ;
1547+ stubStartSession (sessionUrl );
1548+ ErrorDetails errorDetails = ErrorDetails .builder ().build ();
1549+ ApiException original =
1550+ ApiExceptionFactory .createException (
1551+ "HTTP 403" ,
1552+ null ,
1553+ new HttpStatusStatusCode (403 , StatusCode .Code .PERMISSION_DENIED ),
1554+ false ,
1555+ errorDetails );
1556+ IOException suppressed = new IOException ("underlying stream error" );
1557+ original .addSuppressed (suppressed );
1558+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1559+ .thenReturn (ApiFutures .immediateFailedFuture (original ));
1560+
1561+ ResumableUploadFuture <String > future =
1562+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1563+
1564+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1565+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1566+ ApiException cause = (ApiException ) ex .getCause ();
1567+ assertThat (cause .getMessage ()).contains (sessionUrl );
1568+ assertThat (cause .getCause ()).isSameInstanceAs (original );
1569+ assertThat (cause .getErrorDetails ()).isSameInstanceAs (errorDetails );
1570+ assertThat (cause .getSuppressed ()).asList ().contains (suppressed );
1571+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1572+ }
1573+
1574+ @ Test
1575+ void testActionableErrors_recoveryFailure_messageContainsUploadSessionUrl () {
1576+ String sessionUrl = "https://upload.url/recovery-error-test" ;
1577+ stubStartSession (sessionUrl );
1578+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1579+ .thenReturn (
1580+ ApiFutures .immediateFailedFuture (
1581+ createApiException (400 , StatusCode .Code .INVALID_ARGUMENT )));
1582+ when (mockQueryCallable .futureCall (any (QueryStatusRequest .class ), any ()))
1583+ .thenReturn (
1584+ ApiFutures .immediateFailedFuture (
1585+ createApiException (403 , StatusCode .Code .PERMISSION_DENIED )));
1586+
1587+ ResumableUploadFuture <String > future =
1588+ callable .futureCall ("resource-path" , streamOf ("hello" ), null );
1589+
1590+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1591+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1592+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1593+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1594+ }
1595+
1596+ @ Test
1597+ void testActionableErrors_globalTimeoutFailure_messageContainsUploadSessionUrl () {
1598+ String sessionUrl = "https://upload.url/timeout-error-test" ;
1599+ stubStartSession (sessionUrl );
1600+ SettableApiFuture <ChunkUploadResponse <String >> hungChunk = SettableApiFuture .create ();
1601+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ())).thenReturn (hungChunk );
1602+
1603+ ResumableUploadCallSettings settings =
1604+ defaultSettings .toBuilder ().setGlobalTimeout (Duration .ofMillis (50 )).build ();
1605+
1606+ ResumableUploadFuture <String > future =
1607+ callable .futureCall ("resource-path" , streamOf ("hello" ), null , settings );
1608+
1609+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1610+ assertThat (ex .getCause ()).isInstanceOf (DeadlineExceededException .class );
1611+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1612+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1613+ }
1614+
1615+ @ Test
1616+ void testActionableErrors_rewindFailure_surfacesActionableSeekableStreamMessage () {
1617+ String sessionUrl = "https://upload.url/rewind-error-test" ;
1618+ stubStartSession (sessionUrl );
1619+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1620+ .thenReturn (
1621+ ApiFutures .immediateFuture (
1622+ ChunkUploadResponse .create (ResumableUploadStatus .ACTIVE , null )))
1623+ .thenReturn (
1624+ ApiFutures .immediateFailedFuture (
1625+ createApiException (400 , StatusCode .Code .INVALID_ARGUMENT )));
1626+
1627+ when (mockQueryCallable .futureCall (any (QueryStatusRequest .class ), any ()))
1628+ .thenReturn (
1629+ ApiFutures .immediateFuture (
1630+ createQueryResponse (false , 4L , null , ResumableUploadStatus .ACTIVE )));
1631+
1632+ byte [] data = new byte [16 ];
1633+ ResumableUploadFuture <String > future =
1634+ callable .futureCall ("resource-path" , new ByteArrayInputStream (data ), null );
1635+
1636+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1637+ assertThat (ex .getCause ()).isInstanceOf (FailedPreconditionException .class );
1638+ assertThat (ex .getCause ().getMessage ()).contains (sessionUrl );
1639+ assertThat (ex .getCause ().getMessage ()).contains ("seekable stream" );
1640+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1641+ }
1642+
15071643 private static class HttpStatusStatusCode implements StatusCode {
15081644 private final int httpStatus ;
15091645 private final StatusCode .Code code ;
0 commit comments