5858import com .google .api .gax .rpc .testing .FakeCallContext ;
5959import java .io .ByteArrayInputStream ;
6060import java .io .IOException ;
61+ import java .io .InputStream ;
6162import java .nio .charset .StandardCharsets ;
6263import java .time .Duration ;
6364import java .util .List ;
@@ -305,7 +306,7 @@ void testUploadCallable_startFailure_failsFuture() {
305306 callable .futureCall ("resource-path" , payloadOf ("data" ), null );
306307
307308 ExecutionException exception = assertThrows (ExecutionException .class , future ::get );
308- assertThat (exception .getCause ()).isInstanceOf (IllegalStateException .class );
309+ assertThat (exception .getCause ()).isInstanceOf (UnknownException .class );
309310 assertThat (exception .getCause ()).hasMessageThat ().contains ("start failed" );
310311 verifyNoInteractions (mockChunkCallable );
311312 }
@@ -320,7 +321,7 @@ void testUploadCallable_chunkFailure_failsFuture() {
320321 callable .futureCall ("resource-path" , payloadOf ("data" ), null );
321322
322323 ExecutionException exception = assertThrows (ExecutionException .class , future ::get );
323- assertThat (exception .getCause ()).isInstanceOf (IllegalStateException .class );
324+ assertThat (exception .getCause ()).isInstanceOf (UnknownException .class );
324325 assertThat (exception .getCause ()).hasMessageThat ().contains ("chunk error" );
325326 }
326327
@@ -609,7 +610,7 @@ void testRecovery_queryNullOffset_failsFatal() throws Exception {
609610 callable .futureCall ("resource-path" , payloadOf ("hello" ), null );
610611
611612 ExecutionException exception = assertThrows (ExecutionException .class , future ::get );
612- assertThat (exception .getCause ()).isInstanceOf (IllegalStateException .class );
613+ assertThat (exception .getCause ()).isInstanceOf (UnknownException .class );
613614 assertThat (exception .getCause ())
614615 .hasMessageThat ()
615616 .contains ("did not include a committed offset" );
@@ -669,7 +670,7 @@ void testRecovery_offsetBelowBufferBase_failsFatal() throws Exception {
669670 callable .futureCall ("resource-path" , payloadOf ("0123456789abcdef" ), null );
670671
671672 ExecutionException exception = assertThrows (ExecutionException .class , future ::get );
672- assertThat (exception .getCause ()).isInstanceOf (IllegalStateException .class );
673+ assertThat (exception .getCause ()).isInstanceOf (UnknownException .class );
673674 assertThat (exception .getCause ()).hasMessageThat ().contains ("below buffer base offset" );
674675 }
675676
@@ -746,7 +747,7 @@ void testRecovery_queryMissingStatusHeader_failsFatal() throws Exception {
746747 callable .futureCall ("resource-path" , payloadOf ("hello" ), null );
747748
748749 ExecutionException exception = assertThrows (ExecutionException .class , future ::get );
749- assertThat (exception .getCause ()).isInstanceOf (IllegalStateException .class );
750+ assertThat (exception .getCause ()).isInstanceOf (UnknownException .class );
750751 assertThat (exception .getCause ())
751752 .hasMessageThat ()
752753 .contains ("missing X-Goog-Upload-Status header" );
@@ -824,6 +825,7 @@ void testRecovery_serverRejectionWithFinalStatus_failsFatalWithoutRetryOrRecover
824825 assertThat (ex .getCause ()).isInstanceOf (InvalidArgumentException .class );
825826 ApiException cause = (ApiException ) ex .getCause ();
826827 assertThat (cause .getStatusCode ().getTransportCode ()).isEqualTo (400 );
828+ assertThat (cause .getMessage ()).contains (sessionUrl );
827829 verify (mockChunkCallable , times (1 )).futureCall (any (), any ());
828830 verifyNoInteractions (mockQueryCallable );
829831 }
@@ -1259,6 +1261,77 @@ List<ResumableUploadProgress> awaitTerminal() throws InterruptedException {
12591261 }
12601262 }
12611263
1264+ @ Test
1265+ void testActionableErrors_startFailure_preservesOriginalException () {
1266+ ApiException startError = createApiException (401 , StatusCode .Code .UNAUTHENTICATED );
1267+ when (mockStartCallable .futureCall (any (), any ()))
1268+ .thenReturn (ApiFutures .immediateFailedFuture (startError ));
1269+
1270+ ResumableUploadFuture <String > future =
1271+ callable .futureCall ("resource-path" , payloadOf ("hello" ), null );
1272+
1273+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1274+ assertThat (ex .getCause ()).isSameInstanceAs (startError );
1275+ assertThat (future .getUploadSessionUrl ()).isNull ();
1276+ }
1277+
1278+ @ Test
1279+ void testActionableErrors_preservesErrorDetailsAndCauseChain () {
1280+ String sessionUrl = "https://upload.url/chunk-error-details-test" ;
1281+ stubStartSession (sessionUrl );
1282+ ErrorDetails errorDetails = ErrorDetails .builder ().build ();
1283+ IOException transportCause = new IOException ("403 Forbidden" );
1284+ ApiException original =
1285+ ApiExceptionFactory .createException (
1286+ "HTTP 403" ,
1287+ transportCause ,
1288+ new HttpStatusStatusCode (403 , StatusCode .Code .PERMISSION_DENIED ),
1289+ false ,
1290+ errorDetails );
1291+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1292+ .thenReturn (ApiFutures .immediateFailedFuture (original ));
1293+
1294+ ResumableUploadFuture <String > future =
1295+ callable .futureCall ("resource-path" , payloadOf ("hello" ), null );
1296+
1297+ ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
1298+ assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
1299+ ApiException cause = (ApiException ) ex .getCause ();
1300+ assertThat (cause .getMessage ()).contains (sessionUrl );
1301+ assertThat (cause .getCause ()).isSameInstanceAs (transportCause );
1302+ assertThat (cause .getErrorDetails ()).isSameInstanceAs (errorDetails );
1303+ assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1304+ }
1305+
1306+ @ Test
1307+ void testUploadCallable_failureOutcome_attachesCloseExceptionViaAddSuppressed () {
1308+ when (mockStartCallable .futureCall (any (), any ()))
1309+ .thenReturn (ApiFutures .immediateFailedFuture (new IllegalStateException ("upload failed" )));
1310+
1311+ InputStream failingStream =
1312+ new InputStream () {
1313+ @ Override
1314+ public int read () {
1315+ return -1 ;
1316+ }
1317+
1318+ @ Override
1319+ public void close () throws IOException {
1320+ throw new IOException ("stream close error" );
1321+ }
1322+ };
1323+
1324+ ResumableUploadFuture <String > future =
1325+ callable .futureCall ("resource-path" , () -> failingStream , null );
1326+ ExecutionException exception = assertThrows (ExecutionException .class , future ::get );
1327+ assertThat (exception .getCause ()).isInstanceOf (UnknownException .class );
1328+ assertThat (exception .getCause ().getSuppressed ()).asList ().hasSize (1 );
1329+ assertThat (exception .getCause ().getSuppressed ()[0 ]).isInstanceOf (IOException .class );
1330+ assertThat (exception .getCause ().getSuppressed ()[0 ])
1331+ .hasMessageThat ()
1332+ .contains ("stream close error" );
1333+ }
1334+
12621335 private static class HttpStatusStatusCode implements StatusCode {
12631336 private final int httpStatus ;
12641337 private final StatusCode .Code code ;
0 commit comments