Skip to content

Commit 2ca41d8

Browse files
authored
Improve error message for ResponseTransformer.toFile() and AsyncRespo… (#6918)
* Improve error message for ResponseTransformer.toFile() and AsyncResponseTransformer.toFile() when parent directory is missing # Conflicts: # .changes/next-release/bugfix-AWSSDKforJavav2-1f2b503.json * Update error message
1 parent 47087a2 commit 2ca41d8

7 files changed

Lines changed: 143 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Improved error message when `ResponseTransformer.toFile()` or `AsyncResponseTransformer.toFile()` fails because the parent directory does not exist. The error now indicates that the parent directory must be created before calling the method."
6+
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/FileTransformerConfiguration.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public static Builder builder() {
105105
* <p>
106106
* Always create a new file. If the file already exists, {@link FileAlreadyExistsException} will be thrown.
107107
* In the event of an error, the SDK will attempt to delete the file (whatever has been written to it so far).
108+
* The file's parent directories must already exist; the SDK will not auto-create them.
108109
*/
109110
public static FileTransformerConfiguration defaultCreateNew() {
110111
return builder().fileWriteOption(FileWriteOption.CREATE_NEW)
@@ -116,7 +117,8 @@ public static FileTransformerConfiguration defaultCreateNew() {
116117
* Returns the default {@link FileTransformerConfiguration} for {@link FileWriteOption#CREATE_OR_REPLACE_EXISTING}
117118
* <p>
118119
* Create a new file if it doesn't exist, otherwise replace the existing file.
119-
* In the event of an error, the SDK will NOT attempt to delete the file, leaving it as-is
120+
* In the event of an error, the SDK will NOT attempt to delete the file, leaving it as-is.
121+
* The file's parent directories must already exist; the SDK will not auto-create them.
120122
*/
121123
public static FileTransformerConfiguration defaultCreateOrReplaceExisting() {
122124
return builder().fileWriteOption(FileWriteOption.CREATE_OR_REPLACE_EXISTING)
@@ -128,7 +130,8 @@ public static FileTransformerConfiguration defaultCreateOrReplaceExisting() {
128130
* Returns the default {@link FileTransformerConfiguration} for {@link FileWriteOption#CREATE_OR_APPEND_TO_EXISTING}
129131
* <p>
130132
* Create a new file if it doesn't exist, otherwise append to the existing file.
131-
* In the event of an error, the SDK will NOT attempt to delete the file, leaving it as-is
133+
* In the event of an error, the SDK will NOT attempt to delete the file, leaving it as-is.
134+
* The file's parent directories must already exist; the SDK will not auto-create them.
132135
*/
133136
public static FileTransformerConfiguration defaultCreateOrAppend() {
134137
return builder().fileWriteOption(FileWriteOption.CREATE_OR_APPEND_TO_EXISTING)
@@ -179,16 +182,19 @@ public int hashCode() {
179182
public enum FileWriteOption {
180183
/**
181184
* Always create a new file. If the file already exists, {@link FileAlreadyExistsException} will be thrown.
185+
* The file's parent directories must already exist; the SDK will not auto-create them.
182186
*/
183187
CREATE_NEW,
184188

185189
/**
186190
* Create a new file if it doesn't exist, otherwise replace the existing file.
191+
* The file's parent directories must already exist; the SDK will not auto-create them.
187192
*/
188193
CREATE_OR_REPLACE_EXISTING,
189194

190195
/**
191196
* Create a new file if it doesn't exist, otherwise append to the existing file.
197+
* The file's parent directories must already exist; the SDK will not auto-create them.
192198
*/
193199
CREATE_OR_APPEND_TO_EXISTING,
194200

core/sdk-core/src/main/java/software/amazon/awssdk/core/async/AsyncResponseTransformer.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ default String name() {
177177
* SDK will attempt to delete the file (whatever has been written to it so far). If the file already exists, an exception will
178178
* be thrown.
179179
*
180+
* <p>The file's parent directories must already exist. The SDK will not auto-create directories, and a
181+
* {@link java.nio.file.NoSuchFileException} will be thrown if they are missing.
182+
*
180183
* @param path Path to file to write to.
181184
* @param <ResponseT> Pojo Response type.
182185
* @return AsyncResponseTransformer instance.
@@ -190,6 +193,9 @@ static <ResponseT> AsyncResponseTransformer<ResponseT, ResponseT> toFile(Path pa
190193
* Creates an {@link AsyncResponseTransformer} that writes all the content to the given file with the specified
191194
* {@link FileTransformerConfiguration}.
192195
*
196+
* <p>The file's parent directories must already exist. The SDK will not auto-create directories, and a
197+
* {@link java.nio.file.NoSuchFileException} will be thrown if they are missing.
198+
*
193199
* @param path Path to file to write to.
194200
* @param config configuration for the transformer
195201
* @param <ResponseT> Pojo Response type.
@@ -217,6 +223,9 @@ static <ResponseT> AsyncResponseTransformer<ResponseT, ResponseT> toFile(
217223
* SDK will attempt to delete the file (whatever has been written to it so far). If the file already exists, an exception will
218224
* be thrown.
219225
*
226+
* <p>The file's parent directories must already exist. The SDK will not auto-create directories, and a
227+
* {@link java.nio.file.NoSuchFileException} will be thrown if they are missing.
228+
*
220229
* @param file File to write to.
221230
* @param <ResponseT> Pojo Response type.
222231
* @return AsyncResponseTransformer instance.

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/FileAsyncResponseTransformer.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,15 @@ private AsynchronousFileChannel createChannel(Path path) throws IOException {
126126
}
127127

128128
ExecutorService executorService = configuration.executorService().orElse(null);
129-
return AsynchronousFileChannel.open(path, options, executorService);
129+
try {
130+
return AsynchronousFileChannel.open(path, options, executorService);
131+
} catch (NoSuchFileException e) {
132+
if (configuration.fileWriteOption() == WRITE_TO_POSITION) {
133+
throw e;
134+
}
135+
throw new NoSuchFileException(e.getFile(), e.getOtherFile(),
136+
"Verify that the file's parent directories exist. The SDK will not auto-create them.");
137+
}
130138
}
131139

132140
@Override

core/sdk-core/src/main/java/software/amazon/awssdk/core/sync/ResponseTransformer.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ default String name() {
109109

110110
/**
111111
* Creates a response transformer that writes all response content to the specified file. If the file already exists
112-
* then a {@link java.nio.file.FileAlreadyExistsException} will be thrown.
112+
* then a {@link FileAlreadyExistsException} will be thrown.
113+
*
114+
* <p>The file's parent directories must already exist. The SDK will not auto-create directories, and a
115+
* {@link NoSuchFileException} will be thrown if they are missing.
113116
*
114117
* @param path Path to file to write to.
115118
* @param <ResponseT> Type of unmarshalled response POJO.
@@ -124,7 +127,7 @@ public ResponseT transform(ResponseT response, AbortableInputStream inputStream)
124127
Files.copy(inputStream, path);
125128
return response;
126129
} catch (IOException copyException) {
127-
String copyError = "Failed to read response into file: " + path;
130+
String copyError = copyErrorMessage(path, copyException);
128131

129132
if (shouldThrowIOException(copyException)) {
130133
throw new IOException(copyError, copyException);
@@ -167,9 +170,21 @@ static boolean shouldThrowIOException(IOException copyException) {
167170
copyException instanceof AccessDeniedException;
168171
}
169172

173+
static String copyErrorMessage(Path path, IOException copyException) {
174+
String baseMessage = "Failed to read response into file: " + path;
175+
if (copyException instanceof NoSuchFileException) {
176+
return baseMessage + ". Verify that the file's parent directories exist."
177+
+ " The SDK will not auto-create them.";
178+
}
179+
return baseMessage;
180+
}
181+
170182
/**
171183
* Creates a response transformer that writes all response content to the specified file. If the file already exists
172-
* then a {@link java.nio.file.FileAlreadyExistsException} will be thrown.
184+
* then a {@link FileAlreadyExistsException} will be thrown.
185+
*
186+
* <p>The file's parent directories must already exist. The SDK will not auto-create directories, and a
187+
* {@link NoSuchFileException} will be thrown if they are missing.
173188
*
174189
* @param file File to write to.
175190
* @param <ResponseT> Type of unmarshalled response POJO.

core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/FileAsyncResponseTransformerTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,49 @@ void onStreamFailed_shouldCompleteFutureExceptionally() {
318318
assertThat(future).isCompletedExceptionally();
319319
}
320320

321+
private static List<FileTransformerConfiguration> parentDirConfigurations() {
322+
return java.util.Arrays.asList(
323+
FileTransformerConfiguration.defaultCreateNew(),
324+
FileTransformerConfiguration.defaultCreateOrReplaceExisting(),
325+
FileTransformerConfiguration.defaultCreateOrAppend()
326+
);
327+
}
328+
329+
@ParameterizedTest
330+
@MethodSource("parentDirConfigurations")
331+
void parentDirectoryDoesNotExist_throwsWithHelpfulMessage(FileTransformerConfiguration config) {
332+
Path testPath = testFs.getPath("nonexistent-parent", "test_file.txt");
333+
FileAsyncResponseTransformer<String> transformer = new FileAsyncResponseTransformer<>(testPath, config);
334+
335+
CompletableFuture<String> future = transformer.prepare();
336+
transformer.onResponse("foobar");
337+
transformer.onStream(testPublisher("content"));
338+
339+
assertThat(future).failsWithin(1, TimeUnit.SECONDS)
340+
.withThrowableOfType(ExecutionException.class)
341+
.withCauseInstanceOf(NoSuchFileException.class)
342+
.withMessageContaining("Verify that the file's parent directories exist")
343+
.withMessageContaining("The SDK will not auto-create them");
344+
}
345+
346+
@Test
347+
void writeToPosition_fileDoesNotExist_throwsWithHelpfulMessage() {
348+
Path testPath = testFs.getPath("nonexistent_file.txt");
349+
FileAsyncResponseTransformer<String> transformer = new FileAsyncResponseTransformer<>(testPath,
350+
FileTransformerConfiguration.builder()
351+
.failureBehavior(DELETE)
352+
.fileWriteOption(FileWriteOption.WRITE_TO_POSITION)
353+
.build());
354+
355+
CompletableFuture<String> future = transformer.prepare();
356+
transformer.onResponse("foobar");
357+
transformer.onStream(testPublisher("content"));
358+
359+
assertThat(future).failsWithin(1, TimeUnit.SECONDS)
360+
.withThrowableOfType(ExecutionException.class)
361+
.withCauseInstanceOf(NoSuchFileException.class);
362+
}
363+
321364
private static void stubSuccessfulStreaming(String newContent, FileAsyncResponseTransformer<String> transformer) throws Exception {
322365
CompletableFuture<String> future = transformer.prepare();
323366
transformer.onResponse("foobar");
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.sync;
17+
18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
20+
import java.io.ByteArrayInputStream;
21+
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.NoSuchFileException;
24+
import java.nio.file.Path;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.io.TempDir;
27+
import software.amazon.awssdk.http.AbortableInputStream;
28+
29+
30+
class ResponseTransformerToFileTest {
31+
32+
@TempDir
33+
Path tempDir;
34+
35+
@Test
36+
void toFile_parentDirectoryDoesNotExist_throwsWithHelpfulMessage() throws Exception {
37+
Path fileWithMissingParent = tempDir.resolve("nonexistent-parent/subdir/output.wav");
38+
ResponseTransformer<String, String> transformer = ResponseTransformer.toFile(fileWithMissingParent);
39+
40+
AbortableInputStream inputStream = AbortableInputStream.create(
41+
new ByteArrayInputStream("test-content".getBytes(StandardCharsets.UTF_8))
42+
);
43+
44+
assertThatThrownBy(() -> transformer.transform("response", inputStream))
45+
.isInstanceOf(IOException.class)
46+
.hasMessageContaining("Verify that the file's parent directories exist")
47+
.hasMessageContaining("The SDK will not auto-create them")
48+
.hasCauseInstanceOf(NoSuchFileException.class);
49+
}
50+
}

0 commit comments

Comments
 (0)