Skip to content

Commit 70ffd8b

Browse files
committed
Allow calling close after abort on a ResponseInputStream.
Before this change, calling `close` after `abort` would raise an IOException. It's well-understood that streams should be `close`d after they're done being used. This was not possible before this change, if `abort` was called. As a side effect of this change, `abort` now calls `close` on the underlying input stream. This is the behavior of the 1.x SDK, so it is low-risk.
1 parent e5db377 commit 70ffd8b

File tree

4 files changed

+98
-0
lines changed

4 files changed

+98
-0
lines changed
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": "Fixed an issue where invoking `abort` and then `close` on a `ResponseInputStream` would cause the `close` to fail."
6+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import software.amazon.awssdk.core.io.SdkFilterInputStream;
2121
import software.amazon.awssdk.http.Abortable;
2222
import software.amazon.awssdk.http.AbortableInputStream;
23+
import software.amazon.awssdk.utils.IoUtils;
2324
import software.amazon.awssdk.utils.Validate;
2425

2526
/**
@@ -57,10 +58,15 @@ public ResponseT response() {
5758
return response;
5859
}
5960

61+
/**
62+
* Close the underlying connection, dropping all remaining data in the stream, and not leaving the
63+
* connection open to be used for future requests.
64+
*/
6065
@Override
6166
public void abort() {
6267
if (abortable != null) {
6368
abortable.abort();
6469
}
70+
IoUtils.closeQuietly(in, null);
6571
}
6672
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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;
17+
18+
import static org.assertj.core.api.Assertions.assertThatCode;
19+
import static org.mockito.Mockito.never;
20+
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import org.junit.jupiter.api.Test;
24+
import org.mockito.Mockito;
25+
import software.amazon.awssdk.http.Abortable;
26+
import software.amazon.awssdk.http.AbortableInputStream;
27+
28+
class ResponseInputStreamTest {
29+
@Test
30+
void abort_withAbortable_closesUnderlyingStream() throws IOException {
31+
InputStream stream = Mockito.mock(InputStream.class);
32+
Abortable abortable = Mockito.mock(Abortable.class);
33+
AbortableInputStream abortableInputStream = AbortableInputStream.create(stream, abortable);
34+
ResponseInputStream<Object> responseInputStream = new ResponseInputStream<>(new Object(), abortableInputStream);
35+
36+
responseInputStream.abort();
37+
38+
Mockito.verify(abortable).abort();
39+
Mockito.verify(stream).close();
40+
}
41+
42+
@Test
43+
void failedClose_withinAbort_isIgnored() throws IOException {
44+
InputStream stream = Mockito.mock(InputStream.class);
45+
Abortable abortable = Mockito.mock(Abortable.class);
46+
AbortableInputStream abortableInputStream = AbortableInputStream.create(stream, abortable);
47+
ResponseInputStream<Object> responseInputStream = new ResponseInputStream<>(new Object(), abortableInputStream);
48+
49+
Mockito.doThrow(new IOException()).when(stream).close();
50+
assertThatCode(responseInputStream::abort).doesNotThrowAnyException();
51+
52+
Mockito.verify(abortable).abort();
53+
Mockito.verify(stream).close();
54+
}
55+
56+
@Test
57+
void abort_withoutAbortable_closesUnderlyingStream() throws IOException {
58+
InputStream stream = Mockito.mock(InputStream.class);
59+
ResponseInputStream<Object> responseInputStream = new ResponseInputStream<>(new Object(), stream);
60+
61+
responseInputStream.abort();
62+
63+
Mockito.verify(stream).close();
64+
}
65+
66+
@Test
67+
void close_withAbortable_closesUnderlyingStream() throws IOException {
68+
InputStream stream = Mockito.mock(InputStream.class);
69+
Abortable abortable = Mockito.mock(Abortable.class);
70+
AbortableInputStream abortableInputStream = AbortableInputStream.create(stream, abortable);
71+
ResponseInputStream<Object> responseInputStream = new ResponseInputStream<>(new Object(), abortableInputStream);
72+
73+
responseInputStream.close();
74+
75+
Mockito.verify(abortable, never()).abort();
76+
Mockito.verify(stream).close();
77+
}
78+
}

services/s3/src/it/java/software/amazon/awssdk/services/s3/GetObjectIntegrationTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.services.s3;
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatCode;
1920
import static org.junit.Assert.assertEquals;
2021
import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName;
2122

@@ -123,6 +124,13 @@ public void contentRangeIsReturnedForRangeRequests() {
123124
assertThat(stream.response().contentRange()).isEqualTo("bytes 0-1/10000");
124125
}
125126

127+
@Test
128+
public void sync_closeAfterAbort_doesNotThrowException() {
129+
ResponseInputStream<GetObjectResponse> stream = s3.getObject(getObjectRequest);
130+
stream.abort();
131+
assertThatCode(stream::close).doesNotThrowAnyException();
132+
}
133+
126134
private S3Client createClientWithInterceptor(ExecutionInterceptor interceptor) {
127135
return s3ClientBuilder().overrideConfiguration(ClientOverrideConfiguration.builder()
128136
.addExecutionInterceptor(interceptor)

0 commit comments

Comments
 (0)