Skip to content

Commit b8d6abe

Browse files
authored
feat: support chunked transfer encoding (#910)
Currently any time HttpRequest works with encoded data it encodes the data twice. Once for the actual stream and once for checking the length of the stream. Instead, when there is encoding just don't set the content length. This will cause the underlying transports, with a few tweaks, to use Transfer-Encoding: chunked. Fixes #648
1 parent 7d4a048 commit b8d6abe

File tree

9 files changed

+135
-34
lines changed

9 files changed

+135
-34
lines changed

google-http-client-apache-v2/src/main/java/com/google/api/client/http/apache/v2/ApacheHttpRequest.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
import org.apache.http.client.config.RequestConfig;
2424
import org.apache.http.client.methods.HttpRequestBase;
2525

26-
/**
27-
* @author Yaniv Inbar
28-
*/
26+
/** @author Yaniv Inbar */
2927
final class ApacheHttpRequest extends LowLevelHttpRequest {
3028
private final HttpClient httpClient;
3129

@@ -38,11 +36,12 @@ final class ApacheHttpRequest extends LowLevelHttpRequest {
3836
this.httpClient = httpClient;
3937
this.request = request;
4038
// disable redirects as google-http-client handles redirects
41-
this.requestConfig = RequestConfig.custom()
42-
.setRedirectsEnabled(false)
43-
.setNormalizeUri(false)
44-
// TODO(chingor): configure in HttpClientBuilder when available
45-
.setStaleConnectionCheckEnabled(false);
39+
this.requestConfig =
40+
RequestConfig.custom()
41+
.setRedirectsEnabled(false)
42+
.setNormalizeUri(false)
43+
// TODO(chingor): configure in HttpClientBuilder when available
44+
.setStaleConnectionCheckEnabled(false);
4645
}
4746

4847
@Override
@@ -52,19 +51,22 @@ public void addHeader(String name, String value) {
5251

5352
@Override
5453
public void setTimeout(int connectTimeout, int readTimeout) throws IOException {
55-
requestConfig.setConnectTimeout(connectTimeout)
56-
.setSocketTimeout(readTimeout);
54+
requestConfig.setConnectTimeout(connectTimeout).setSocketTimeout(readTimeout);
5755
}
5856

5957
@Override
6058
public LowLevelHttpResponse execute() throws IOException {
6159
if (getStreamingContent() != null) {
62-
Preconditions.checkArgument(request instanceof HttpEntityEnclosingRequest,
60+
Preconditions.checkState(
61+
request instanceof HttpEntityEnclosingRequest,
6362
"Apache HTTP client does not support %s requests with content.",
6463
request.getRequestLine().getMethod());
6564
ContentEntity entity = new ContentEntity(getContentLength(), getStreamingContent());
6665
entity.setContentEncoding(getContentEncoding());
6766
entity.setContentType(getContentType());
67+
if (getContentLength() == -1) {
68+
entity.setChunked(true);
69+
}
6870
((HttpEntityEnclosingRequest) request).setEntity(entity);
6971
}
7072
request.setConfig(requestConfig.build());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2019 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
15+
package com.google.api.client.http.apache.v2;
16+
17+
import static org.junit.Assert.assertEquals;
18+
import static org.junit.Assert.assertFalse;
19+
import static org.junit.Assert.assertTrue;
20+
21+
import com.google.api.client.http.ByteArrayContent;
22+
import com.google.api.client.http.HttpContent;
23+
import com.google.api.client.http.InputStreamContent;
24+
import com.google.api.client.testing.http.apache.MockHttpClient;
25+
import java.io.ByteArrayInputStream;
26+
import java.nio.charset.StandardCharsets;
27+
import java.util.Arrays;
28+
import org.junit.Test;
29+
30+
public class ApacheHttpRequestTest {
31+
32+
@Test
33+
public void testContentLengthSet() throws Exception {
34+
HttpExtensionMethod base = new HttpExtensionMethod("POST", "http://www.google.com");
35+
ApacheHttpRequest request = new ApacheHttpRequest(new MockHttpClient(), base);
36+
HttpContent content =
37+
new ByteArrayContent("text/plain", "sample".getBytes(StandardCharsets.UTF_8));
38+
request.setStreamingContent(content);
39+
request.setContentLength(content.getLength());
40+
request.execute();
41+
42+
assertFalse(base.getEntity().isChunked());
43+
assertEquals(6, base.getEntity().getContentLength());
44+
}
45+
46+
@Test
47+
public void testChunked() throws Exception {
48+
byte[] buf = new byte[300];
49+
Arrays.fill(buf, (byte) ' ');
50+
HttpExtensionMethod base = new HttpExtensionMethod("POST", "http://www.google.com");
51+
ApacheHttpRequest request = new ApacheHttpRequest(new MockHttpClient(), base);
52+
HttpContent content = new InputStreamContent("text/plain", new ByteArrayInputStream(buf));
53+
request.setStreamingContent(content);
54+
request.execute();
55+
56+
assertTrue(base.getEntity().isChunked());
57+
assertEquals(-1, base.getEntity().getContentLength());
58+
}
59+
}

google-http-client-apache-v2/src/test/java/com/google/api/client/http/apache/v2/ApacheHttpTransportTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import java.io.OutputStream;
3535
import java.net.InetSocketAddress;
3636
import java.nio.charset.StandardCharsets;
37-
import java.util.Random;
3837
import java.util.concurrent.atomic.AtomicBoolean;
3938
import java.util.concurrent.atomic.AtomicInteger;
4039
import org.apache.http.Header;
@@ -123,8 +122,8 @@ private void subtestUnsupportedRequestsWithContent(ApacheHttpRequest request, St
123122
throws IOException {
124123
try {
125124
execute(request);
126-
fail("expected " + IllegalArgumentException.class);
127-
} catch (IllegalArgumentException e) {
125+
fail("expected " + IllegalStateException.class);
126+
} catch (IllegalStateException e) {
128127
// expected
129128
assertEquals(e.getMessage(),
130129
"Apache HTTP client does not support " + method + " requests with content.");

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package com.google.api.client.http;
1616

1717
import com.google.api.client.util.Beta;
18-
import com.google.api.client.util.IOUtils;
1918
import com.google.api.client.util.LoggingStreamingContent;
2019
import com.google.api.client.util.ObjectParser;
2120
import com.google.api.client.util.Preconditions;
@@ -141,7 +140,7 @@ public final class HttpRequest {
141140

142141
/** HTTP request URL. */
143142
private GenericUrl url;
144-
143+
145144
/** Timeout in milliseconds to establish a connection or {@code 0} for an infinite timeout. */
146145
private int connectTimeout = 20 * 1000;
147146

@@ -172,7 +171,7 @@ public final class HttpRequest {
172171
/** The {@link BackOffPolicy} to use between retry attempts or {@code null} for none. */
173172
@Deprecated @Beta private BackOffPolicy backOffPolicy;
174173

175-
/** Whether to automatically follow redirects ({@code true} by default). */
174+
/** Whether to automatically follow redirects ({@code true} by default). */
176175
private boolean followRedirects = true;
177176

178177
/** Whether to use raw redirect URLs ({@code false} by default). */
@@ -698,15 +697,13 @@ public HttpRequest setFollowRedirects(boolean followRedirects) {
698697
return this;
699698
}
700699

701-
/**
702-
* Return whether to use raw redirect URLs.
703-
*/
700+
/** Return whether to use raw redirect URLs. */
704701
public boolean getUseRawRedirectUrls() {
705702
return useRawRedirectUrls;
706703
}
707704

708705
/**
709-
* Sets whether to use raw redirect URLs.
706+
* Sets whether to use raw redirect URLs.
710707
*
711708
* <p>The default value is {@code false}.
712709
*/
@@ -938,7 +935,7 @@ public HttpResponse execute() throws IOException {
938935
final boolean contentRetrySupported = streamingContent == null || content.retrySupported();
939936
if (streamingContent != null) {
940937
final String contentEncoding;
941-
final long contentLength;
938+
long contentLength = -1;
942939
final String contentType = content.getType();
943940
// log content
944941
if (loggable) {
@@ -953,7 +950,6 @@ public HttpResponse execute() throws IOException {
953950
} else {
954951
contentEncoding = encoding.getName();
955952
streamingContent = new HttpEncodingStreamingContent(streamingContent, encoding);
956-
contentLength = contentRetrySupported ? IOUtils.computeLength(streamingContent) : -1;
957953
}
958954
// append content headers to log buffer
959955
if (loggable) {
@@ -1222,7 +1218,7 @@ private static void addSpanAttribute(Span span, String key, String value) {
12221218
span.putAttribute(key, AttributeValue.stringAttributeValue(value));
12231219
}
12241220
}
1225-
1221+
12261222
private static String getVersion() {
12271223
// attempt to read the library's version from a properties file generated during the build
12281224
// this value should be read and cached for later use

google-http-client/src/main/java/com/google/api/client/http/apache/ApacheHttpRequest.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@
2525
import org.apache.http.params.HttpConnectionParams;
2626
import org.apache.http.params.HttpParams;
2727

28-
/**
29-
* @author Yaniv Inbar
30-
*/
28+
/** @author Yaniv Inbar */
3129
final class ApacheHttpRequest extends LowLevelHttpRequest {
3230
private final HttpClient httpClient;
3331

@@ -54,12 +52,16 @@ public void setTimeout(int connectTimeout, int readTimeout) throws IOException {
5452
@Override
5553
public LowLevelHttpResponse execute() throws IOException {
5654
if (getStreamingContent() != null) {
57-
Preconditions.checkArgument(request instanceof HttpEntityEnclosingRequest,
55+
Preconditions.checkState(
56+
request instanceof HttpEntityEnclosingRequest,
5857
"Apache HTTP client does not support %s requests with content.",
5958
request.getRequestLine().getMethod());
6059
ContentEntity entity = new ContentEntity(getContentLength(), getStreamingContent());
6160
entity.setContentEncoding(getContentEncoding());
6261
entity.setContentType(getContentType());
62+
if (getContentLength() == -1) {
63+
entity.setChunked(true);
64+
}
6365
((HttpEntityEnclosingRequest) request).setEntity(entity);
6466
}
6567
return new ApacheHttpResponse(request, httpClient.execute(request));

google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpRequest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public void addHeader(String name, String value) {
4949
connection.addRequestProperty(name, value);
5050
}
5151

52+
@VisibleForTesting
53+
String getRequestProperty(String name) {
54+
return connection.getRequestProperty(name);
55+
}
56+
5257
@Override
5358
public void setTimeout(int connectTimeout, int readTimeout) {
5459
connection.setReadTimeout(readTimeout);

google-http-client/src/main/java/com/google/api/client/testing/http/javanet/MockHttpURLConnection.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,8 @@ public String getHeaderField(String name) {
201201
List<String> values = headers.get(name);
202202
return values == null ? null : values.get(0);
203203
}
204+
205+
public int getChunkLength() {
206+
return chunkLength;
207+
}
204208
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,7 @@ public LowLevelHttpResponse execute() throws IOException {
987987
if (expectGZip) {
988988
assertEquals(HttpEncodingStreamingContent.class, getStreamingContent().getClass());
989989
assertEquals("gzip", getContentEncoding());
990-
assertEquals(25, getContentLength());
990+
assertEquals(-1, getContentLength());
991991
} else {
992992
assertFalse(
993993
getStreamingContent().getClass().equals(HttpEncodingStreamingContent.class));
@@ -1227,7 +1227,9 @@ public void testExecute_curlLogger() throws Exception {
12271227
if (message.startsWith("curl")) {
12281228
found = true;
12291229
assertTrue(message.contains("curl -v --compressed -H 'Accept-Encoding: gzip'"));
1230-
assertTrue(message.contains("-H 'User-Agent: Google-HTTP-Java-Client/" + HttpRequest.VERSION + " (gzip)'"));
1230+
assertTrue(
1231+
message.contains(
1232+
"-H 'User-Agent: Google-HTTP-Java-Client/" + HttpRequest.VERSION + " (gzip)'"));
12311233
assertTrue(message.contains("' -- 'http://google.com/#q=a'\"'\"'b'\"'\"'c'"));
12321234
}
12331235
}
@@ -1258,7 +1260,9 @@ public void testExecute_curlLoggerWithContentEncoding() throws Exception {
12581260
found = true;
12591261
assertTrue(message.contains("curl -v --compressed -X POST -H 'Accept-Encoding: gzip'"));
12601262
assertTrue(message.contains("-H 'User-Agent: " + HttpRequest.USER_AGENT_SUFFIX + "'"));
1261-
assertTrue(message.contains("-H 'Content-Type: text/plain; charset=UTF-8' -H 'Content-Encoding: gzip'"));
1263+
assertTrue(
1264+
message.contains(
1265+
"-H 'Content-Type: text/plain; charset=UTF-8' -H 'Content-Encoding: gzip'"));
12621266
assertTrue(message.contains("-d '@-' -- 'http://google.com/#q=a'\"'\"'b'\"'\"'c' << $$$"));
12631267
}
12641268
}

google-http-client/src/test/java/com/google/api/client/http/javanet/NetHttpRequestTest.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package com.google.api.client.http.javanet;
22

3-
import static org.junit.Assert.assertEquals;
4-
import static org.junit.Assert.assertTrue;
5-
import static org.junit.Assert.fail;
3+
import static org.junit.Assert.*;
64

5+
import com.google.api.client.http.ByteArrayContent;
76
import com.google.api.client.http.HttpContent;
87
import com.google.api.client.http.InputStreamContent;
98
import com.google.api.client.http.LowLevelHttpResponse;
@@ -15,6 +14,7 @@
1514
import java.io.InputStream;
1615
import java.io.OutputStream;
1716
import java.net.URL;
17+
import java.nio.charset.StandardCharsets;
1818
import java.util.concurrent.TimeoutException;
1919
import org.junit.Test;
2020

@@ -203,4 +203,34 @@ public void close() throws IOException {
203203
assertEquals("Error during close", e.getMessage());
204204
}
205205
}
206+
207+
@Test
208+
public void testChunkedLengthSet() throws Exception {
209+
MockHttpURLConnection connection = new MockHttpURLConnection(new URL(HttpTesting.SIMPLE_URL));
210+
connection.setRequestMethod("POST");
211+
NetHttpRequest request = new NetHttpRequest(connection);
212+
InputStream is = NetHttpRequestTest.class.getClassLoader().getResourceAsStream("file.txt");
213+
HttpContent content = new InputStreamContent("text/plain", is);
214+
request.setStreamingContent(content);
215+
request.setContentEncoding("gzip");
216+
request.execute();
217+
218+
assertEquals(4096, connection.getChunkLength());
219+
assertNull(request.getRequestProperty("Content-Length"));
220+
}
221+
222+
@Test
223+
public void testChunkedLengthNotSet() throws Exception {
224+
MockHttpURLConnection connection = new MockHttpURLConnection(new URL(HttpTesting.SIMPLE_URL));
225+
connection.setRequestMethod("POST");
226+
NetHttpRequest request = new NetHttpRequest(connection);
227+
HttpContent content =
228+
new ByteArrayContent("text/plain", "sample".getBytes(StandardCharsets.UTF_8));
229+
request.setStreamingContent(content);
230+
request.setContentLength(content.getLength());
231+
request.execute();
232+
233+
assertEquals(connection.getChunkLength(), -1);
234+
assertEquals("6", request.getRequestProperty("Content-Length"));
235+
}
206236
}

0 commit comments

Comments
 (0)