Skip to content

Commit 087a428

Browse files
authored
feat: wrap GZIPInputStream for connection reuse (#840)
If a connection is closed and there are some bytes that have not been read that connection can't be reused. Now GZIPInputStream will have all of its bytes read on close automatically to promote connection reuse. Cherry-picked: #749 Fixes: #367
1 parent 1522eb5 commit 087a428

File tree

4 files changed

+152
-1
lines changed

4 files changed

+152
-1
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2019 Google LLC
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+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.api.client.http;
18+
19+
import com.google.common.io.ByteStreams;
20+
import java.io.FilterInputStream;
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
24+
/**
25+
* This class in meant to wrap an {@link InputStream} so that all bytes in the steam are read and
26+
* discarded on {@link InputStream#close()}. This ensures that the underlying connection has the
27+
* option to be reused.
28+
*/
29+
final class ConsumingInputStream extends FilterInputStream {
30+
private boolean closed = false;
31+
32+
ConsumingInputStream(InputStream inputStream) {
33+
super(inputStream);
34+
}
35+
36+
@Override
37+
public void close() throws IOException {
38+
if (!closed && in != null) {
39+
try {
40+
ByteStreams.exhaust(this);
41+
super.in.close();
42+
} finally {
43+
this.closed = true;
44+
}
45+
}
46+
}
47+
}

google-http-client/src/main/java/com/google/api/client/http/HttpResponse.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,8 @@ public InputStream getContent() throws IOException {
331331
if (!returnRawInputStream
332332
&& contentEncoding != null
333333
&& contentEncoding.contains("gzip")) {
334-
lowLevelResponseContent = new GZIPInputStream(lowLevelResponseContent);
334+
lowLevelResponseContent =
335+
new ConsumingInputStream(new GZIPInputStream(lowLevelResponseContent));
335336
}
336337
// logging (wrap content with LoggingInputStream)
337338
Logger logger = HttpTransport.LOGGER;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2019 Google LLC
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+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.api.client.http;
18+
19+
import static org.junit.Assert.assertEquals;
20+
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.nio.charset.Charset;
24+
import java.nio.charset.StandardCharsets;
25+
import org.junit.Test;
26+
27+
public class ConsumingInputStreamTest {
28+
29+
@Test
30+
public void testClose_drainsBytesOnClose() throws IOException {
31+
MockInputStream mockInputStream = new MockInputStream("abc123".getBytes(StandardCharsets.UTF_8));
32+
InputStream consumingInputStream = new ConsumingInputStream(mockInputStream);
33+
34+
assertEquals(6, mockInputStream.getBytesToRead());
35+
36+
// read one byte
37+
consumingInputStream.read();
38+
assertEquals(5, mockInputStream.getBytesToRead());
39+
40+
// closing the stream should read the remaining bytes
41+
consumingInputStream.close();
42+
assertEquals(0, mockInputStream.getBytesToRead());
43+
}
44+
45+
private class MockInputStream extends InputStream {
46+
private int bytesToRead;
47+
48+
MockInputStream(byte[] data) {
49+
this.bytesToRead = data.length;
50+
}
51+
52+
@Override
53+
public int read() throws IOException {
54+
if (bytesToRead == 0) {
55+
return -1;
56+
}
57+
bytesToRead--;
58+
return 1;
59+
}
60+
61+
int getBytesToRead() {
62+
return bytesToRead;
63+
}
64+
}
65+
}

google-http-client/src/test/java/com/google/api/client/http/HttpResponseTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
import java.io.ByteArrayOutputStream;
2727
import java.io.IOException;
2828
import java.lang.reflect.Type;
29+
import java.nio.charset.StandardCharsets;
2930
import java.text.NumberFormat;
3031
import java.util.Arrays;
3132
import java.util.logging.Level;
3233
import java.util.zip.GZIPInputStream;
34+
import java.util.zip.GZIPOutputStream;
3335
import junit.framework.TestCase;
3436

3537
/**
@@ -457,4 +459,40 @@ public LowLevelHttpResponse execute() throws IOException {
457459
"it should not decompress stream",
458460
request.execute().getContent() instanceof GZIPInputStream);
459461
}
462+
463+
public void testGetContent_gzipEncoding_finishReading() throws IOException {
464+
byte[] dataToCompress = "abcd".getBytes(StandardCharsets.UTF_8);
465+
byte[] mockBytes;
466+
try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(dataToCompress.length)) {
467+
GZIPOutputStream zipStream = new GZIPOutputStream((byteStream));
468+
zipStream.write(dataToCompress);
469+
zipStream.close();
470+
mockBytes = byteStream.toByteArray();
471+
}
472+
final MockLowLevelHttpResponse mockResponse = new MockLowLevelHttpResponse();
473+
mockResponse.setContent(mockBytes);
474+
mockResponse.setContentEncoding("gzip");
475+
mockResponse.setContentType("text/plain");
476+
477+
HttpTransport transport =
478+
new MockHttpTransport() {
479+
@Override
480+
public LowLevelHttpRequest buildRequest(String method, final String url)
481+
throws IOException {
482+
return new MockLowLevelHttpRequest() {
483+
@Override
484+
public LowLevelHttpResponse execute() throws IOException {
485+
return mockResponse;
486+
}
487+
};
488+
}
489+
};
490+
HttpRequest request =
491+
transport.createRequestFactory().buildHeadRequest(HttpTesting.SIMPLE_GENERIC_URL);
492+
HttpResponse response = request.execute();
493+
TestableByteArrayInputStream output = (TestableByteArrayInputStream) mockResponse.getContent();
494+
assertFalse(output.isClosed());
495+
assertEquals("abcd", response.parseAsString());
496+
assertTrue(output.isClosed());
497+
}
460498
}

0 commit comments

Comments
 (0)