1818import com .google .cloud .storage .StorageOptions ;
1919import java .io .File ;
2020import java .io .InputStream ;
21+ import java .net .SocketTimeoutException ;
2122import java .nio .channels .Channels ;
2223import java .util .Map ;
2324import net .snowflake .client .api .exception .SnowflakeSQLException ;
2930import net .snowflake .client .internal .log .SFLoggerFactory ;
3031import net .snowflake .client .internal .util .SFPair ;
3132import net .snowflake .common .core .SqlState ;
33+ import org .apache .http .HttpStatus ;
3234
3335class GCSDefaultAccessStrategy implements GCSAccessStrategy {
3436 private static final SFLogger logger = SFLoggerFactory .getLogger (GCSDefaultAccessStrategy .class );
@@ -209,32 +211,55 @@ public boolean handleStorageException(
209211 // re-wraps it before re-throwing. Walking the chain ensures that a transient 503 or similar
210212 // GCS error is always treated as retryable regardless of how many layers of wrapping were
211213 // added.
212- StorageException se = null ;
213- if (ex instanceof StorageException ) {
214- se = (StorageException ) ex ;
215- } else {
216- Throwable cause = ex .getCause ();
217- while (cause != null ) {
218- if (cause instanceof StorageException ) {
219- se = (StorageException ) cause ;
220- break ;
221- }
222- cause = cause .getCause ();
223- }
224- if (se != null ) {
225- logger .debug (
226- "GCSDefaultAccessStrategy: found StorageException (HTTP {}) wrapped inside {} during {};"
227- + " treating as retryable" ,
228- se .getCode (),
229- ex .getClass ().getSimpleName (),
230- operation );
231- }
214+ StorageException se = SnowflakeUtil .findFirstCauseOfType (ex , StorageException .class );
215+ if (se != null && se != ex ) {
216+ logger .debug (
217+ "GCSDefaultAccessStrategy: found StorageException (HTTP {}) wrapped inside {} during {};"
218+ + " treating as retryable" ,
219+ se .getCode (),
220+ ex .getClass ().getSimpleName (),
221+ operation );
232222 }
233223
234224 if (se != null ) {
235225 // NOTE: this code path only handles Access token based operations.
236226 // Presigned URL operations do not raise StorageException.
237227
228+ // Non-transient client errors must not be retried during upload/download, mirroring
229+ // S3ErrorHandler.isClientException400Or404 (400 and 404 only).
230+ // 401 is excluded here because it is handled below via token refresh.
231+ //
232+ // WHY THE SCOPE IS LIMITED TO UPLOAD AND DOWNLOAD:
233+ // When OVERWRITE=FALSE, SnowflakeFileTransferAgent.filterExistingFiles() calls
234+ // storageClient.listObjects() (returns an empty collection on miss — never a 404) and
235+ // then storageClient.getObjectMetadata() on each listed object to compare digests.
236+ // If the object disappears between the list and the metadata fetch (a race condition),
237+ // GCSDefaultAccessStrategy.getObjectMetadata() throws StorageProviderException wrapping a
238+ // synthetic StorageException(404). The SnowflakeFileTransferAgent.compareAndSkipRemoteFiles()
239+ // SNOW-14521 handler attempts to recognise this 404 via isServiceException404(), but that
240+ // method only checks SdkServiceException (AWS SDK); it does not recognise the GCS-native
241+ // StorageException, so the exception is re-thrown and eventually reaches
242+ // handleStorageException() with operation="compareRemoteFiles". Applying the
243+ // immediate-throw rule for 404 here would break that retry path — the correct behaviour is
244+ // to retry the list/compare cycle via the normal max-retries logic.
245+ // NOTE: for the upload path itself, OVERWRITE=FALSE produces only a LIST (GET ?prefix=...)
246+ // call and, if the file is found, skips the upload before getObjectMetadata() is ever
247+ // reached. No 404 is generated by the upload-path existence check.
248+ if ((StorageHelper .UPLOAD .equals (operation ) || StorageHelper .DOWNLOAD .equals (operation ))
249+ && (se .getCode () == HttpStatus .SC_BAD_REQUEST
250+ || se .getCode () == HttpStatus .SC_NOT_FOUND )) {
251+ throw new SnowflakeSQLLoggedException (
252+ queryId ,
253+ session ,
254+ SqlState .SYSTEM_ERROR ,
255+ StorageHelper .getOperationException (operation ).getMessageCode (),
256+ se ,
257+ operation ,
258+ se .getCode (),
259+ se .getMessage (),
260+ se .getReason ());
261+ }
262+
238263 // If we have exceeded the max number of retries, propagate the error.
239264 if (retryCount > gcsClient .getMaxRetries ()) {
240265 logger .error (
@@ -243,8 +268,8 @@ public boolean handleStorageException(
243268 gcsClient .getMaxRetries (),
244269 se .getCode (),
245270 se .getMessage (),
246- operation );
247- logger . error ( "Stack trace: " , ex );
271+ operation ,
272+ ex );
248273 throw new SnowflakeSQLLoggedException (
249274 queryId ,
250275 session ,
@@ -260,8 +285,8 @@ public boolean handleStorageException(
260285 "Encountered exception ({}) during {}, retry count: {}" ,
261286 se .getMessage (),
262287 operation ,
263- retryCount );
264- logger . debug ( "Stack trace: " , ex );
288+ retryCount ,
289+ ex );
265290
266291 // exponential backoff up to a limit
267292 int backoffInMillis = gcsClient .getRetryBackoffMin ();
@@ -294,13 +319,29 @@ public boolean handleStorageException(
294319 }
295320 return true ;
296321 } else {
322+ // InterruptedException and SocketTimeoutException are handled by the outer retry loop in
323+ // SnowflakeGCSClient.handleStorageException (retry within max retries, throw when exhausted).
324+ // Return false so that logic is preserved rather than bypassed.
325+ if (ex instanceof InterruptedException
326+ || SnowflakeUtil .getRootCause (ex ) instanceof SocketTimeoutException ) {
327+ return false ;
328+ }
297329 logger .error (
298- "GCSDefaultAccessStrategy: unhandled exception type {} during {}, not retrying: {} " ,
330+ "GCSDefaultAccessStrategy: unhandled exception type {} during {}, not retrying" ,
299331 ex .getClass ().getName (),
300332 operation ,
301- ex .getMessage ());
302- logger .error ("Stack trace: " , ex );
303- return false ;
333+ ex );
334+ // Re-throw as-is to avoid double-wrapping an already well-formed SQL exception.
335+ if (ex instanceof SnowflakeSQLException ) {
336+ throw (SnowflakeSQLException ) ex ;
337+ }
338+ throw new SnowflakeSQLLoggedException (
339+ queryId ,
340+ session ,
341+ SqlState .SYSTEM_ERROR ,
342+ StorageHelper .getOperationException (operation ).getMessageCode (),
343+ ex ,
344+ "Encountered exception during " + operation + ": " + ex .getMessage ());
304345 }
305346 }
306347
0 commit comments