• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2022 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // 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
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 package com.google.fcp.client.http;
15 
16 import static com.google.common.base.Strings.nullToEmpty;
17 
18 import com.google.common.base.Ascii;
19 import com.google.common.io.CountingInputStream;
20 import com.google.fcp.client.CallFromNativeWrapper;
21 import com.google.fcp.client.http.HttpClientForNative.HttpRequestHandle;
22 import com.google.fcp.client.http.HttpClientForNativeImpl.UncheckedHttpClientForNativeException;
23 import com.google.rpc.Code;
24 import com.google.rpc.Status;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.net.CookieHandler;
29 import java.net.HttpURLConnection;
30 import java.net.ProtocolException;
31 import java.net.SocketTimeoutException;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.concurrent.CancellationException;
35 import java.util.concurrent.ExecutionException;
36 import java.util.concurrent.ExecutorService;
37 import java.util.concurrent.Future;
38 import java.util.zip.GZIPInputStream;
39 import javax.annotation.Nullable;
40 import javax.annotation.concurrent.GuardedBy;
41 
42 /**
43  * An implementation of {@link HttpRequestHandle} that uses {@link HttpURLConnection} (the
44  * implementation of which is provided via the {@link HttpURLConnectionFactory} indirection) for
45  * issuing network requests.
46  *
47  * <p>Note: this class is non-final to allow the native callback methods defined in {@link
48  * HttpRequestHandle} to be overridden in unit tests. This is class is otherwise not meant to be
49  * extended and should hence be considered effectively 'final'.
50  */
51 public class HttpRequestHandleImpl extends HttpRequestHandle {
52 
53   /**
54    * A factory for creating an {@link HttpURLConnection} for a given URI.
55    *
56    * <p>To use the system's default {@link HttpURLConnection} implementation one can simply use
57    * {@code new URL(uri).openConnection()} as the factory implementation.
58    */
59   public interface HttpURLConnectionFactory {
createUrlConnection(String uri)60     HttpURLConnection createUrlConnection(String uri) throws IOException;
61   }
62 
63   // The Content-Length header name.
64   private static final String CONTENT_LENGTH_HEADER = "Content-Length";
65   // The Accept-Encoding header name.
66   private static final String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
67   // The Content-Encoding response header name.
68   private static final String CONTENT_ENCODING_HEADER = "Content-Encoding";
69   // The Accept-Encoding and Content-Encoding value to indicate gzip-based compression.
70   private static final String GZIP_ENCODING = "gzip";
71   // The Transfer-Encoding header name.
72   private static final String TRANSFER_ENCODING_HEADER = "Transfer-Encoding";
73   // The Transfer-Encoding value indicating "chunked" encoding.
74   private static final String CHUNKED_TRANSFER_ENCODING = "chunked";
75 
76   /** Used to indicate that the request was invalid in some way. */
77   private static final class InvalidHttpRequestException extends Exception {
InvalidHttpRequestException(String message)78     private InvalidHttpRequestException(String message) {
79       super(message);
80     }
81 
InvalidHttpRequestException(String message, Throwable cause)82     private InvalidHttpRequestException(String message, Throwable cause) {
83       super(message, cause);
84     }
85   }
86 
87   /**
88    * Used to indicate that the request was cancelled or encountered an unrecoverable error in the
89    * middle of an operation. The request should be aborted without invoking any further callbacks to
90    * the native layer.
91    */
92   private static final class AbortRequestException extends Exception {}
93 
94   private enum State {
95     /**
96      * The state when this object is created, but before it has been passed to {@link
97      * HttpClientForNative#performRequests}.
98      */
99     NOT_STARTED,
100     /**
101      * The state before any response headers have been received. Errors should go to the {@link
102      * #onResponseError} callback.
103      */
104     BEFORE_RESPONSE_HEADERS,
105     /**
106      * The state after any response headers have been received. Errors should go to the {@link
107      * #onResponseBodyError} callback.
108      */
109     AFTER_RESPONSE_HEADERS,
110     /**
111      * The state after the request was finished (either successfully, with an error, or via
112      * cancellation), and no more callbacks should be invoked.
113      */
114     CLOSED
115   }
116 
117   private final JniHttpRequest request;
118   private final CallFromNativeWrapper callFromNativeWrapper;
119   private final ExecutorService executorService;
120   private final HttpURLConnectionFactory urlConnectionFactory;
121   private final int connectTimeoutMs;
122   private final int readTimeoutMs;
123   private final int requestBodyChunkSizeBytes;
124   private final int responseBodyChunkSizeBytes;
125   private final int responseBodyGzipBufferSizeBytes;
126   private final boolean callDisconnectWhenCancelled;
127   private final boolean supportAcceptEncodingHeader;
128   private final double estimatedHttp2HeaderCompressionRatio;
129 
130   // Until we have an actual connection, this is a no-op.
131   @GuardedBy("this")
132   private Runnable disconnectRunnable = () -> {};
133 
134   @GuardedBy("this")
135   private State state = State.NOT_STARTED;
136 
137   @GuardedBy("this")
138   @Nullable
139   private Future<?> ongoingWork;
140 
141   // These are "volatile" and not synchronized so that they can be read easily from any thread even
142   // if the lock is currently held. They're only incremented from a single thread, so their being
143   // volatile is sufficient to safely increment/update them.
144   private volatile long sentHeaderBytes = 0;
145   private volatile long sentBodyBytes = 0;
146   private volatile long receivedHeaderBytes = 0;
147   private volatile long receivedBodyBytes = 0;
148   private volatile boolean requestUsedHttp2Heuristic = false;
149 
150   /**
151    * Creates a new handle representing a single request. See {@link HttpClientForNativeImpl} for a
152    * description of the parameters.
153    *
154    * @param request the {@link JniHttpRequest} the handle is being created for.
155    * @param callFromNativeWrapper the wrapper to use for all calls that arrive over JNI, to ensure
156    *     uncaught exceptions are handled correctly.
157    * @param executorService the {@link ExecutorService} to use for background work.
158    * @param urlConnectionFactory the factory to use to instance new {@link HttpURLConnection}s.
159    * @param connectTimeoutMs the value to use with {@link HttpURLConnection#setConnectTimeout(int)}.
160    *     If this is -1 then {@code setConnectTimeout} will not be called at all.
161    * @param readTimeoutMs the value to use with {@link HttpURLConnection#setReadTimeout(int)}. If
162    *     this is -1 then {@code setReadTimeout} will not be called at all.
163    *     <p>If {@code getInputStream().read(...)} or other methods like {@code getResponseCode()}
164    *     take longer than this amount of time, they will throw a {@link
165    *     java.net.SocketTimeoutException} and request will fail. Setting it to -1 will result in an
166    *     infinite timeout being used.
167    *     <p>Note that this only affects the reads of the response body, and does not affect the
168    *     writes of the request body.
169    * @param requestBodyChunkSizeBytes the value to use with {@link
170    *     HttpURLConnection#setChunkedStreamingMode(int)}, when chunked transfer encoding is used to
171    *     upload request bodies. This also determines the amount of request body data we'll read from
172    *     the native layer before pushing it onto the network's {@link java.io.OutputStream}.
173    * @param responseBodyChunkSizeBytes determines the amount of response body data we'll try to read
174    *     from the network's {@link java.io.InputStream} (or from the {@link
175    *     java.util.zip.GZIPInputStream} wrapping the network's {@code InputStream}) before pushing
176    *     it to the native layer.
177    * @param responseBodyGzipBufferSizeBytes determines the amount of response body data the {@link
178    *     java.util.zip.GZIPInputStream} wrapping the network's {@link java.io.InputStream} will try
179    *     to read before starting another round of decompression (in case we receive a compressed
180    *     response body that we need to decompress on the fly).
181    * @param callDisconnectWhenCancelled whether to call {@link HttpURLConnection#disconnect()} (from
182    *     a different thread than the request is being run on) when a request gets cancelled. See
183    *     note in {@link HttpRequestHandleImpl#close()}.
184    * @param supportAcceptEncodingHeader whether to set the "Accept-Encoding" request header by
185    *     default. Some {@link HttpURLConnection} implementations don't allow setting it, and this
186    *     flag allows turning that behavior off. When this setting is false, the assumption is that
187    *     the implementation at the very least sets "Accept-Encoding: gzip" (as required by the C++
188    *     `HttpClient` contract).
189    * @param estimatedHttp2HeaderCompressionRatio the compression ratio to account for in the
190    *     calculation of sent/received bytes estimates for the header data, in case HTTP/2 is used
191    *     for the request. HTTP/2 supports HPACK, and hence counting the header data in uncompressed
192    *     form likely results in over-estimates. This only affects requests that are determined to
193    *     have used HTTP/2, which is based on the somewhat fragile heuristic of whether {@link
194    *     HttpURLConnection#getResponseMessage()} is empty (since HTTP/2 does not support status line
195    *     'reason phrases').
196    */
HttpRequestHandleImpl( JniHttpRequest request, CallFromNativeWrapper callFromNativeWrapper, ExecutorService executorService, HttpURLConnectionFactory urlConnectionFactory, int connectTimeoutMs, int readTimeoutMs, int requestBodyChunkSizeBytes, int responseBodyChunkSizeBytes, int responseBodyGzipBufferSizeBytes, boolean callDisconnectWhenCancelled, boolean supportAcceptEncodingHeader, double estimatedHttp2HeaderCompressionRatio)197   public HttpRequestHandleImpl(
198       JniHttpRequest request,
199       CallFromNativeWrapper callFromNativeWrapper,
200       ExecutorService executorService,
201       HttpURLConnectionFactory urlConnectionFactory,
202       int connectTimeoutMs,
203       int readTimeoutMs,
204       int requestBodyChunkSizeBytes,
205       int responseBodyChunkSizeBytes,
206       int responseBodyGzipBufferSizeBytes,
207       boolean callDisconnectWhenCancelled,
208       boolean supportAcceptEncodingHeader,
209       double estimatedHttp2HeaderCompressionRatio) {
210     this.request = request;
211     this.callFromNativeWrapper = callFromNativeWrapper;
212     this.executorService = executorService;
213     this.urlConnectionFactory = urlConnectionFactory;
214     this.connectTimeoutMs = connectTimeoutMs;
215     this.readTimeoutMs = readTimeoutMs;
216     this.requestBodyChunkSizeBytes = requestBodyChunkSizeBytes;
217     this.responseBodyChunkSizeBytes = responseBodyChunkSizeBytes;
218     this.responseBodyGzipBufferSizeBytes = responseBodyGzipBufferSizeBytes;
219     this.callDisconnectWhenCancelled = callDisconnectWhenCancelled;
220     this.supportAcceptEncodingHeader = supportAcceptEncodingHeader;
221     this.estimatedHttp2HeaderCompressionRatio = estimatedHttp2HeaderCompressionRatio;
222   }
223 
224   @Override
close()225   public final void close() {
226     // This method is called when the request should be cancelled and/or is otherwise not
227     // needed anymore. It may be called from any thread.
228     callFromNativeWrapper.wrapVoidCall(
229         () -> {
230           synchronized (this) {
231             // If the request was already closed, then this means that the request was either
232             // already interrupted before, or that the request completed successfully. In both
233             // cases there's nothing left to do for us.
234             if (state == State.CLOSED) {
235               return;
236             }
237             // Otherwise, this indicates that the request is being *cancelled* while it was still
238             // running.
239 
240             // We mark the connection closed, to prevent any further callbacks to the native layer
241             // from being issued. We do this *before* invoking the callback, just in case our
242             // invoking the callback causes this close() method to be invoked again by the native
243             // layer (we wouldn't want to enter an infinite loop)
244             State oldState = state;
245             state = State.CLOSED;
246             // We signal the closure/cancellation to the native layer right away, using the
247             // appropriate callback for the state we were in.
248             doError(Code.CANCELLED, "request cancelled via close()", oldState);
249             // We unblock the blocked thread on which HttpClientForNativeImpl#performRequests was
250             // called (although that thread may be blocked on other, still-pending requests).
251             if (ongoingWork != null) {
252               ongoingWork.cancel(/* mayInterruptIfRunning=*/ true);
253             }
254 
255             // Note that HttpURLConnection isn't documented to be thread safe, and hence it isn't
256             // 100% clear that calling its #disconnect() method from a different thread (as we are
257             // about to do here) will correctly either. However, it seems to be the only way to
258             // interrupt an ongoing request when it is blocked writing to or reading from the
259             // network socket.
260             //
261             // At least on Android the OkHttp-based implementation does seem to be thread safe (it
262             // uses OkHttp's HttpEngine.cancel() method, which is thread safe). The JDK
263             // implementation seems to not be thread safe (but behaves well enough?). The
264             // callDisconnectWhenCancelled parameter can be used to control this behavior.
265             if (callDisconnectWhenCancelled) {
266               disconnectRunnable.run();
267             }
268 
269             // Handling cancellations/closures this way ensures that the native code is unblocked
270             // even before the network requests have been fully aborted. Any still-pending HTTP
271             // connections will be cleaned up in their corresponding background threads.
272           }
273         });
274   }
275 
276   @Override
getTotalSentReceivedBytes()277   public byte[] getTotalSentReceivedBytes() {
278     double headerCompressionRatio =
279         requestUsedHttp2Heuristic ? estimatedHttp2HeaderCompressionRatio : 1.0;
280     // Note that this estimate of sent/received bytes is not necessarily monotonically increasing:
281     // - We'll initially estimate the amount of received response body bytes based on the bytes we
282     //   observe in the response InputStream (which may count the uncompressed response bytes). This
283     //   will account, as best as possible, for how much has data been received so far (incl. in
284     //   case the request gets cancelled mid-flight), although it may be an over-estimate due to not
285     //   accounting for response body compression (depending on the HttpURLConnection
286     //   implementation, e.g. in case of Cronet's).
287     // - Once the request has completed successfully, we'll estimate the received response body
288     //   bytes based on the Content-Length response header, if there was one. This gives us a chance
289     //   to revise our estimate down to a more accurate value, if the HttpURLConnection
290     //   implementation exposes the original Content-Length header to us (e.g. in the case of
291     //   Cronet).
292     // - Once we know from the response headers that the request used HTTP/2, we'll apply the header
293     //   compression ratio. But before we know that, we don't apply it.
294     //
295     // Note that the estimates we provide here also won't take into account various other sources of
296     // network usage: the bytes transmitted to establish TLS channels, request/responses for
297     // followed HTTP redirects, HTTP/1.1-to-HTTP/2 upgrades etc.
298     return JniHttpSentReceivedBytes.newBuilder()
299         .setSentBytes((long) (sentHeaderBytes * headerCompressionRatio) + sentBodyBytes)
300         .setReceivedBytes((long) (receivedHeaderBytes * headerCompressionRatio) + receivedBodyBytes)
301         .build()
302         .toByteArray();
303   }
304 
performRequest()305   final synchronized void performRequest() {
306     if (state != State.NOT_STARTED) {
307       throw new IllegalStateException("must not call perform() more than once");
308     }
309     state = State.BEFORE_RESPONSE_HEADERS;
310     ongoingWork = executorService.submit(this::runRequestToCompletion);
311   }
312 
waitForRequestCompletion()313   final void waitForRequestCompletion() {
314     // Get a copy of the Future, if it is set. Then call .get() without holding the lock.
315     Future<?> localOngoingWork;
316     synchronized (this) {
317       if (ongoingWork == null) {
318         throw new IllegalStateException("must not call waitForCompletion() before perform()");
319       }
320       localOngoingWork = ongoingWork;
321     }
322     try {
323       localOngoingWork.get();
324     } catch (ExecutionException e) {
325       // This shouldn't happen, since the run(...) method shouldn't throw any exceptions. If one
326       // does get thrown, it is a RuntimeException or Error, in which case we'll just let it bubble
327       // up to the uncaught exception handler.
328       throw new UncheckedHttpClientForNativeException("unexpected exception", e);
329     } catch (InterruptedException e) {
330       // This shouldn't happen, since no one should be interrupting the calling thread.
331       throw new UncheckedHttpClientForNativeException("unexpected interruption", e);
332     } catch (CancellationException e) {
333       // Do nothing. This will happen when a request gets cancelled in the middle of execution, but
334       // in those cases there's nothing left for us to do, and we should just gracefully return.
335       // This will allow #performRequests(...) to be unblocked, while the background thread may
336       // still be cleaning up some resources.
337     }
338   }
339 
340   /** Convenience method for checking for the closed state, in a synchronized fashion. */
isClosed()341   private synchronized boolean isClosed() {
342     return state == State.CLOSED;
343   }
344 
345   /**
346    * Convenience method for checking for the closed state in a synchronized fashion, throwing an
347    * {@link AbortRequestException} if the request is closed.
348    */
checkClosed()349   private synchronized void checkClosed() throws AbortRequestException {
350     if (state == State.CLOSED) {
351       throw new AbortRequestException();
352     }
353   }
354 
355   /**
356    * Calls either the {@link #onResponseError} or {@link #onResponseBodyError} callback, including
357    * the originating Java exception description in the status message. Which callback is used
358    * depends on the current {@link #state}.
359    */
doError(String message, Exception e)360   private synchronized void doError(String message, Exception e) {
361     // We mark the state as CLOSED, since no more callbacks should be invoked after signaling an
362     // error. We do this before issuing the callback to the native layer, to ensure that if that
363     // call results in another call to the Java layer, we don't emit any callbacks anymore.
364     State oldState = state;
365     state = State.CLOSED;
366     Code code = Code.UNAVAILABLE;
367     if (e instanceof SocketTimeoutException) {
368       code = Code.DEADLINE_EXCEEDED;
369     } else if (e instanceof InvalidHttpRequestException) {
370       code = Code.INVALID_ARGUMENT;
371     }
372     doError(code, String.format("%s (%s)", message, e), oldState);
373   }
374 
375   @GuardedBy("this")
doError(Code code, String message, State state)376   private void doError(Code code, String message, State state) {
377     byte[] error =
378         Status.newBuilder().setCode(code.getNumber()).setMessage(message).build().toByteArray();
379     switch (state) {
380       case BEFORE_RESPONSE_HEADERS:
381         onResponseError(error);
382         break;
383       case AFTER_RESPONSE_HEADERS:
384         onResponseBodyError(error);
385         break;
386       case NOT_STARTED:
387       case CLOSED:
388         // If the request had already been closed, or if it hadn't been passed to {@link
389         // HttpClientForNative#performRequests} yet, then we shouldn't issue any (further)
390         // callbacks.
391         break;
392     }
393   }
394 
395   /** Calls the {@link #readRequestBody} callback, but only if the request isn't closed yet. */
doReadRequestBody( byte[] buffer, long requestedBytes, int[] actualBytesRead)396   private synchronized void doReadRequestBody(
397       byte[] buffer, long requestedBytes, int[] actualBytesRead) throws AbortRequestException {
398     // If the request has already been closed, then we shouldn't issue any further callbacks.
399     checkClosed();
400     checkCallToNativeResult(readRequestBody(buffer, requestedBytes, actualBytesRead));
401   }
402 
403   /** Calls the {@link #onResponseStarted} callback, but only if the request isn't closed yet. */
doOnResponseStarted(byte[] responseProto)404   private synchronized void doOnResponseStarted(byte[] responseProto) throws AbortRequestException {
405     // Ensure that we call the onResponseStarted callback *and* update the object state as a
406     // single atomic transaction, so that any errors/cancellations occurring before or after
407     // this block result in the correct error callback being called.
408 
409     // If the request has already been closed, then we shouldn't issue any further callbacks.
410     checkClosed();
411     // After this point, any errors we signal to the native layer should go through
412     // 'onResponseBodyError', so we update the object state. We do this before invoking the
413     // callback, to ensure that if our call into the native layer causes a call back into Java
414     // that then triggers an error callback, we invoke the right one.
415     state = State.AFTER_RESPONSE_HEADERS;
416     checkCallToNativeResult(onResponseStarted(responseProto));
417   }
418 
419   /** Calls the {@link #onResponseBody} callback, but only if the request isn't closed yet. */
doOnResponseBody(byte[] buffer, int bytesAvailable)420   private synchronized void doOnResponseBody(byte[] buffer, int bytesAvailable)
421       throws AbortRequestException {
422     // If the request has already been closed, then we shouldn't issue any further callbacks.
423     checkClosed();
424     checkCallToNativeResult(onResponseBody(buffer, bytesAvailable));
425   }
426 
427   /** Calls the {@link #onResponseCompleted} callback, but only if the request isn't closed yet. */
doOnResponseCompleted(long originalContentLengthHeader)428   private synchronized void doOnResponseCompleted(long originalContentLengthHeader) {
429     // If the request has already been closed, then we shouldn't issue any further callbacks.
430     if (state == State.CLOSED) {
431       return;
432     }
433     // If we did receive a Content-Length header, then once we've fully completed the request, we
434     // can use it to estimate the total received bytes (and it will be the most accurate estimate
435     // available to us).
436     //
437     // E.g. the Cronet HttpURLConnection implementation will return the original Content-Length
438     // header, even though it decompresses any response body Content-Encoding for us and doesn't let
439     // use see the original compressed bytes.
440     //
441     // If there was no Content-Length header at all, then we must go by our own calculation of the
442     // number of received bytes (i.e. based on the bytes we observed in the response InputStream).
443     if (originalContentLengthHeader > -1) {
444       receivedBodyBytes = originalContentLengthHeader;
445     }
446     // If the request hadn't already been closed, it should be considered closed now (since we're
447     // about to call the final callback).
448     state = State.CLOSED;
449     onResponseCompleted();
450   }
451 
452   /**
453    * Transitions to the CLOSED {@link #state} and throws an AbortRequestException, if the given
454    * result from a call to the native layer is false.
455    */
456   @GuardedBy("this")
checkCallToNativeResult(boolean result)457   private void checkCallToNativeResult(boolean result) throws AbortRequestException {
458     if (!result) {
459       // If any call to the native layer fails, then we shouldn't invoke any more callbacks.
460       state = State.CLOSED;
461       throw new AbortRequestException();
462     }
463   }
464 
runRequestToCompletion()465   private void runRequestToCompletion() {
466     // If we're already closed by the time the background thread started executing this method,
467     // there's nothing left to do for us.
468     if (isClosed()) {
469       return;
470     }
471 
472     // Create the HttpURLConnection instance (this usually doesn't do any real work yet, even
473     // though it is declared to throw IOException).
474     HttpURLConnection connection;
475     try {
476       connection = urlConnectionFactory.createUrlConnection(request.getUri());
477     } catch (IOException e) {
478       doError("failure during connection creation", e);
479       return;
480     }
481 
482     // Register a runnable that will allow us to cancel an ongoing request from a different
483     // thread.
484     synchronized (this) {
485       disconnectRunnable = connection::disconnect;
486     }
487 
488     // From this point on we should call connection.disconnect() at the end of this method
489     // invocation, *except* when the request reaches a successful end (see comment below).
490     boolean doDisconnect = true;
491     try {
492       // Set and validate connection parameters (timeouts, HTTP method, request body, etc.).
493       String acceptEncodingHeader = findRequestHeader(ACCEPT_ENCODING_HEADER);
494       long requestContentLength;
495       try {
496         requestContentLength = parseContentLengthHeader(findRequestHeader(CONTENT_LENGTH_HEADER));
497         configureConnection(connection, requestContentLength, acceptEncodingHeader);
498       } catch (InvalidHttpRequestException e) {
499         doError("invalid request", e);
500         return;
501       }
502 
503       // If there is a request body then start sending it. This is usually when the actual network
504       // connection is first established (subject to the #getRequestConnectTimeoutMs).
505       if (request.getHasBody()) {
506         try {
507           sendRequestBody(connection, requestContentLength);
508         } catch (IOException e) {
509           doError("failure during request body send", e);
510           return;
511         }
512       }
513 
514       // Check one more time, before waiting on the response headers, if the request has already
515       // been cancelled (to avoid starting any blocking network IO we can't easily interrupt).
516       checkClosed();
517 
518       // If there was no request body, then this will establish the connection (subject to the
519       // #getRequestConnectTimeoutMs). If there was a request body, this will be a noop.
520       try {
521         connection.connect();
522       } catch (IOException e) {
523         doError("failure during connect", e);
524         return;
525       }
526 
527       // Wait for the request headers to be received (subject to #getRequestReadTimeOutMs).
528       ResponseHeadersWithMetadata response;
529       try {
530         response = receiveResponseHeaders(connection, acceptEncodingHeader);
531       } catch (IOException e) {
532         doError("failure during response header receive", e);
533         return;
534       }
535       doOnResponseStarted(response.responseProto.toByteArray());
536 
537       try {
538         receiveResponseBody(connection, response.shouldDecodeGzip);
539       } catch (IOException e) {
540         doError("failure during response body receive", e);
541         return;
542       }
543       // Note that we purposely don't call connection.disconnect() once we reach this point, since
544       // we will have gracefully finished the request (e.g. by having readall of its response data),
545       // and this means that the underlying socket/connection may be reused for other connections to
546       // the same endpoint. Calling connection.disconnect() would prevent such connection reuse,
547       // which can be detrimental to the overall throughput. The underlying HttpURLConnection
548       // implementation will eventually reap the socket if doesn't end up being reused within a set
549       // amount of time.
550       doDisconnect = false;
551       doOnResponseCompleted(response.originalContentLengthHeader);
552     } catch (AbortRequestException e) {
553       // Nothing left for us to do.
554     } finally {
555       if (doDisconnect) {
556         connection.disconnect();
557       }
558       // At this point we will either have reached the end of the request successfully (in which
559       // case doOnResponseCompleted will have updated the object state to CLOSED), or we will have
560       // hit a AbortRequestException (in which case the state will already have been set to CLOSED),
561       // or we will have signaled an error (which will have set the state to CLOSED as well).
562       // Hence we don't have to modify the object state here anymore.
563     }
564   }
565 
566   /** Returns the HTTP request method we should use, as a string. */
getRequestMethod()567   private String getRequestMethod() {
568     switch (request.getMethod()) {
569       case HTTP_METHOD_HEAD:
570         return "HEAD";
571       case HTTP_METHOD_GET:
572         return "GET";
573       case HTTP_METHOD_POST:
574         return "POST";
575       case HTTP_METHOD_PUT:
576         return "PUT";
577       case HTTP_METHOD_PATCH:
578         return "PATCH";
579       case HTTP_METHOD_DELETE:
580         return "DELETE";
581       default:
582         // This shouldn't happen, as it would indicate a bug in either this code or the native C++
583         // code calling us.
584         throw new UncheckedHttpClientForNativeException(
585             String.format("unexpected method: %s", request.getMethod().getNumber()));
586     }
587   }
588 
589   /**
590    * Finds the given header (case-insensitively) and returns its value, if there is one. Otherwise
591    * returns null.
592    */
593   @Nullable
findRequestHeader(String name)594   private String findRequestHeader(String name) {
595     for (JniHttpHeader header : request.getExtraHeadersList()) {
596       if (Ascii.equalsIgnoreCase(name, header.getName())) {
597         return header.getValue();
598       }
599     }
600     return null;
601   }
602 
603   /**
604    * Tries to parse a "Content-Length" header value and returns it as a long, if it isn't null.
605    * Otherwise returns -1.
606    */
parseContentLengthHeader(String contentLengthHeader)607   private long parseContentLengthHeader(String contentLengthHeader)
608       throws InvalidHttpRequestException {
609     if (contentLengthHeader == null) {
610       return -1;
611     }
612     try {
613       return Long.parseLong(contentLengthHeader);
614     } catch (NumberFormatException e) {
615       throw new InvalidHttpRequestException(
616           String.format("invalid Content-Length request header value: %s", contentLengthHeader), e);
617     }
618   }
619 
620   /**
621    * Configures the {@link HttpURLConnection} object before it is used to establish the actual
622    * network connection.
623    */
624   @SuppressWarnings("NonAtomicVolatileUpdate")
configureConnection( HttpURLConnection connection, long requestContentLength, @Nullable String acceptEncodingHeader)625   private void configureConnection(
626       HttpURLConnection connection,
627       long requestContentLength,
628       @Nullable String acceptEncodingHeader)
629       throws InvalidHttpRequestException {
630     String requestMethod = getRequestMethod();
631     try {
632       connection.setRequestMethod(requestMethod);
633     } catch (ProtocolException e) {
634       // This should never happen, as we take care to only call this method with appropriate
635       // parameters.
636       throw new UncheckedHttpClientForNativeException("unexpected ProtocolException", e);
637     }
638     for (JniHttpHeader header : request.getExtraHeadersList()) {
639       // Note that we use addRequestProperty rather than setRequestProperty, to ensure that
640       // request headers that occur multiple times are properly specified (rather than just the
641       // last value being specified).
642       connection.addRequestProperty(header.getName(), header.getValue());
643     }
644     // The C++ `HttpClient` contract requires us to set the Accept-Encoding header, if there isn't
645     // one provided by the native layer. Note that on Android the HttpURLConnection implementation
646     // does this by default, but the JDK's implementation does not. Note that by setting this header
647     // we must also handle the response InputStream data correctly (by inflating it, if the
648     // Content-Encoding indicates the data is compressed).
649     // Some HttpURLConnection implementations (such as Cronet's) don't allow setting this header,
650     // and print out a warning if you do. The supportAcceptEncodingHeader allows turning this
651     // behavior off (thereby avoiding the warning being logged).
652     if (supportAcceptEncodingHeader && acceptEncodingHeader == null) {
653       connection.setRequestProperty(ACCEPT_ENCODING_HEADER, GZIP_ENCODING);
654     } else if (!supportAcceptEncodingHeader && acceptEncodingHeader != null) {
655       throw new InvalidHttpRequestException("cannot support Accept-Encoding header");
656     }
657 
658     if (connectTimeoutMs >= 0) {
659       connection.setConnectTimeout(connectTimeoutMs);
660     }
661     if (readTimeoutMs >= 0) {
662       connection.setReadTimeout(readTimeoutMs);
663     }
664 
665     connection.setDoInput(true);
666     if (request.getHasBody()) {
667       connection.setDoOutput(true);
668       if (requestContentLength >= 0) {
669         // If the Content-Length header is set then we don't have to use Transfer-Encoding, since
670         // we know the size of the request body ahead of time.
671         connection.setFixedLengthStreamingMode(requestContentLength);
672       } else {
673         // If we don't know the size of the request body ahead of time, we should turn on
674         // "Transfer-Encoding: chunked" using the following method.
675         connection.setChunkedStreamingMode(requestBodyChunkSizeBytes);
676       }
677     } else if (requestContentLength > 0) {
678       // If getHasBody() is false but a non-zero Content-Length header is set, then something went
679       // wrong in the native layer.
680       throw new InvalidHttpRequestException("Content-Length > 0 but no request body available");
681     }
682 
683     // As per the interface contract in C++'s http_client.h, we should not use any caches.
684     connection.setUseCaches(false);
685     // As per the interface contract in C++'s http_client.h, we should follow redirects.
686     connection.setInstanceFollowRedirects(true);
687 
688     // Ensure that no system-wide CookieHandler was installed, since we must not store any cookies.
689     if (CookieHandler.getDefault() != null) {
690       throw new IllegalStateException("must not set a CookieHandler");
691     }
692 
693     // Count the request headers as part of the sent bytes. We do this before we actually open the
694     // connection, so that if the connection fails to be established we still account for the
695     // possibly already-transmitted data.
696     //
697     // Note that if the implementation uses HTTP2 with HPACK header compression this could lead to
698     // an overestimation of the total bytes sent. The estimatedHttp2HeaderCompressionRatio parameter
699     // can be used to account for this heuristically.
700     //
701     // If HTTP/2 is used, then some of our estimates will also be overestimates since we assume that
702     // headers are terminated by \r\n lines etc., while HTTP/2 generally represents headers more
703     // compactly. To avoid complicating things too much, we don't account for that.
704     //
705     // Aside from not accounting for HTTP/2 and header compression, some request headers may also be
706     // set by the HttpUrlConnection implementation which we cannot observe here, and hence we won't
707     // be counting those either. Hence, this number could be both an over or under-estimate, and
708     // should really be considered a best-effort estimate.
709     //
710     // Note that while it might seem we could use getRequestProperties() to get the actual request
711     // headers (incl. implementation-specified ones), this isn't actually the case for most
712     // HttpURLConnection implementations (and some implementations don't return anything from
713     // getRequestProperties(), even if we've already called addRequestProperty()).
714     // First, account for the HTTP request status line.
715     sentHeaderBytes +=
716         requestMethod.length()
717             + " ".length()
718             + request.getUri().length()
719             + " HTTP/1.1\r\n".length();
720     // Then account for each header we know is will be included.
721     for (JniHttpHeader header : request.getExtraHeadersList()) {
722       // Each entry should count the lengths of the header name + header value (rather than only
723       // counting the header name length once), since duplicated headers are likely to be sent in
724       // separate header lines (rather than being coalesced into a single header line by the
725       // HttpURLConnection implementation).
726       sentHeaderBytes +=
727           header.getName().length() + ": ".length() + header.getValue().length() + "\r\n".length();
728     }
729     // Account for the \r\n characters at the end of the request headers.
730     sentHeaderBytes += "\r\n".length();
731   }
732 
733   /**
734    * Sends the request body (received from the native layer via the JNI callbacks) to the server
735    * after establishing a connection (blocking until all request body data has been written to the
736    * network, or an error occurs).
737    *
738    * @param connection the HttpURLConnection to send the request body for.
739    * @param requestContentLength the length of the request body if it is known ahead of time, or -1
740    *     if the request body's length is not known ahead of time.
741    */
742   @SuppressWarnings("NonAtomicVolatileUpdate")
sendRequestBody(HttpURLConnection connection, long requestContentLength)743   private void sendRequestBody(HttpURLConnection connection, long requestContentLength)
744       throws IOException, AbortRequestException {
745     // Check one more time, before issuing the request, if it's already been cancelled (to avoid
746     // starting any blocking network IO we can't easily interrupt).
747     checkClosed();
748 
749     // Note that we don't wrap the OutputStream in a BufferedOutputStream, since we already write
750     // data to the unbuffered OutputStream in fairly large chunks at a time, so adding another
751     // buffering layer in between isn't helpful.
752     //
753     // The call to getOutputStream or OutputStream.write() is what will establish the actual
754     // network connection.
755     try (OutputStream outputStream = connection.getOutputStream()) {
756       // Allocate a buffer for reading the request body data into via JNI.
757       byte[] buffer = new byte[calculateRequestBodyBufferSize(requestContentLength)];
758       // Allocate an array for the native layer to write the number of actually read bytes into.
759       // Because arrays are mutable, this effectively serves as an 'output parameter', allowing the
760       // native code to return this bit of information in addition to its primary success/failure
761       // return value.
762       int[] actualBytesRead = new int[1];
763       while (true) {
764         // Read data from native. This may be very fast, but may also block on disk IO and/or
765         // on-the-fly payload compression.
766         doReadRequestBody(buffer, buffer.length, actualBytesRead);
767         // The native layer signals the end of the request body data by returning -1 as the
768         // "actually read bytes" value (this corresponds to C++'s `HttpRequest::ReadBody` returning
769         // `OUT_OF_RANGE`).
770         if (actualBytesRead[0] == -1) {
771           // End of data reached (successfully).
772           break;
773         }
774         // Otherwise, the native layer is required to have read at least 1 byte into our buffer at
775         // this point (and hence actualBytesRead[0] will be >= 1).
776 
777         // Account for the data we're about to send in our 'sent bytes' stats. We do this before we
778         // write it to the output stream (so that this over rather than under-estimates the number,
779         // in case we get interrupted mid-write).
780         sentBodyBytes += actualBytesRead[0];
781 
782         // Write the data from the native layer to the network socket.
783         outputStream.write(buffer, 0, actualBytesRead[0]);
784 
785         // Before trying to read another chunk of data, make sure that the request hasn't been
786         // aborted yet.
787         checkClosed();
788       }
789       // Flush the stream before we close it, for good measure.
790       outputStream.flush();
791     }
792     // We're done uploading.
793   }
794 
calculateRequestBodyBufferSize(long requestContentLength)795   private int calculateRequestBodyBufferSize(long requestContentLength) {
796     // If the request body size is known ahead of time, and is smaller than the chunk size we
797     // otherwise would use, then we allocate a buffer of just the exact size we need. If the
798     // request body size is unknown or too large, then we use a set chunk buffer size to read one
799     // chunk at a time.
800     if (requestContentLength > 0 && requestContentLength < requestBodyChunkSizeBytes) {
801       // This cast from long to int is safe, because we know requestContentLength is smaller than
802       // the int bufferSize at this point.
803       return (int) requestContentLength;
804     }
805     return requestBodyChunkSizeBytes;
806   }
807 
808   private static final class ResponseHeadersWithMetadata {
809     private final JniHttpResponse responseProto;
810     private final boolean shouldDecodeGzip;
811     private final long originalContentLengthHeader;
812 
ResponseHeadersWithMetadata( JniHttpResponse responseProto, boolean shouldDecodeGzip, long originalContentLengthHeader)813     ResponseHeadersWithMetadata(
814         JniHttpResponse responseProto, boolean shouldDecodeGzip, long originalContentLengthHeader) {
815       this.responseProto = responseProto;
816       this.shouldDecodeGzip = shouldDecodeGzip;
817       this.originalContentLengthHeader = originalContentLengthHeader;
818     }
819   }
820 
821   /**
822    * Receives the response headers from the server (blocking until that data is available, or an
823    * error occurs), and passes it to the native layer via the JNI callbacks.
824    */
825   @SuppressWarnings("NonAtomicVolatileUpdate")
receiveResponseHeaders( HttpURLConnection connection, String originalAcceptEncodingHeader)826   private ResponseHeadersWithMetadata receiveResponseHeaders(
827       HttpURLConnection connection, String originalAcceptEncodingHeader) throws IOException {
828     // This call will block until the response headers are received (or throw if an error occurred
829     // before headers were received, or if no response header data is received before
830     // #getRequestReadTimeOutMs).
831     int responseCode = connection.getResponseCode();
832 
833     // If the original headers we received from the native layer did not include an Accept-Encoding
834     // header, then *if we specified an "Accept-Encoding" header ourselves and subsequently received
835     // an encoded response body* we should a) remove the Content-Encoding header (since they refer
836     // to the encoded data, not the decoded data we will return to the native layer), and b) decode
837     // the response body data before returning it to the native layer. Note that if we did receive
838     // an "Accept-Encoding" header (even if it specified "gzip"), we must not auto-decode the
839     // response body and we should also leave the headers alone.
840     boolean shouldDecodeGzip = false;
841     if (supportAcceptEncodingHeader && originalAcceptEncodingHeader == null) {
842       // We need to strip the headers, if the body is encoded. Determine if it is encoded first.
843       for (Map.Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
844         List<String> headerValues = header.getValue();
845         if (Ascii.equalsIgnoreCase(CONTENT_ENCODING_HEADER, nullToEmpty(header.getKey()))
846             && !headerValues.isEmpty()
847             && Ascii.equalsIgnoreCase(GZIP_ENCODING, nullToEmpty(headerValues.get(0)))) {
848           shouldDecodeGzip = true;
849           break;
850         }
851       }
852     }
853 
854     JniHttpResponse.Builder response = JniHttpResponse.newBuilder();
855     response.setCode(responseCode);
856 
857     // Account for the response status line in the 'received bytes' stats.
858     String responseMessage = connection.getResponseMessage();
859     // Note that responseMessage could be null or empty if an HTTP/2 implementation is used (since
860     // HTTP/2 doesn't have 'reason phrases' in the status line anymore, only codes).
861     responseMessage = nullToEmpty(responseMessage);
862     receivedHeaderBytes += "HTTP/1.1 XXX ".length() + responseMessage.length() + "\r\n".length();
863     // Add two bytes to account for the \r\n at the end of the response headers.
864     receivedHeaderBytes += "\r\n".length();
865 
866     // If the response message was empty, then we assume that the request used HTTP/2. This is a
867     // flawed heuristic, but the best we have available.
868     requestUsedHttp2Heuristic = responseMessage.isEmpty();
869 
870     // Now let's process the response headers.
871     long receivedContentLength = -1;
872     for (Map.Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
873       // First, let's account for the received headers in our 'received bytes' stats. See note about
874       // counting bytes for request headers above, which applies similarly to response
875       // headers.
876       //
877       // Note that for some HttpURLConnection implementations the HTTP response status line may be
878       // included in the getHeadersField() result under the null header key, while others don't
879       // include it at all. We just skip counting the status line from getHeaderFields() sinec we
880       // already accounted for it above.
881       if (header.getKey() == null) {
882         continue;
883       }
884       // Count the bytes for all the headers (including accounting for the colon, space, and
885       // newlines that would've been sent over the wire).
886       for (String headerValue : header.getValue()) {
887         receivedHeaderBytes +=
888             header.getKey() == null ? 0 : (header.getKey().length() + ": ".length());
889         receivedHeaderBytes += headerValue == null ? 0 : headerValue.length();
890         // Account for the \r\n chars at the end of the header.
891         receivedHeaderBytes += "\r\n".length();
892       }
893 
894       // Now let's skip headers we shouldn't return to the C++ layer.
895       //
896       // The HttpURLConnection implementation generally unchunks response bodies that used
897       // "Transfer-Encoding: chunked". However, while Android's implementation also then removes the
898       // "Transfer-Encoding" header, the JDK implementation does not. Since the HttpClient contract
899       // requires us to remove that header, we explicitly filter it out here.
900       //
901       // Finally, if the response will automatically be gzip-decoded by us, then we must redact any
902       // Content-Encoding header too.
903       if ((Ascii.equalsIgnoreCase(TRANSFER_ENCODING_HEADER, header.getKey())
904               && header.getValue().size() == 1
905               && Ascii.equalsIgnoreCase(
906                   CHUNKED_TRANSFER_ENCODING, nullToEmpty(header.getValue().get(0))))
907           || (shouldDecodeGzip
908               && Ascii.equalsIgnoreCase(CONTENT_ENCODING_HEADER, header.getKey()))) {
909         continue;
910       }
911       // Also, the "Content-Length" value returned by HttpURLConnection may or may not correspond to
912       // the response body data we will see via  + " - " + receivedBodyBytesgetInputStream() (e.g.
913       // it may reflect the length of
914       // the previously compressed data, even if the data is already decompressed for us when we
915       // read it from the InputStream). Hence, we ignore it as well. We do so even though the C++
916       // `HttpClient` asks us to leave it unredacted, because its value cannot be interpreted
917       // consistently. However, if the "Content-Length" header *is* available, then we do use it to
918       // estimate the network bytes we've received (but only once the request has completed
919       // successfully).
920       if (Ascii.equalsIgnoreCase(CONTENT_LENGTH_HEADER, header.getKey())) {
921         if (header.getValue().size() == 1) {
922           try {
923             receivedContentLength = Long.parseLong(header.getValue().get(0));
924           } catch (NumberFormatException e) {
925             // ignore
926           }
927         }
928         continue;
929       }
930 
931       // Pass the remaining headers to the C++ layer.
932       for (String headerValue : header.getValue()) {
933         response.addHeaders(
934             JniHttpHeader.newBuilder().setName(header.getKey()).setValue(headerValue));
935       }
936     }
937 
938     // If we receive a positive cache hit (i.e. HTTP_NOT_MODIFIED), then the response will not have
939     // a body even though the "Content-Encoding" header may still be set. In such cases we shouldn't
940     // try pass the InputStream to a GZIPInputStream (in the receiveResponseBody function below),
941     // since GZIPInputStream would crash on the 0-byte stream. Note that while we disable any
942     // HttpURLConnection-level cache explicitly in this file, it's still possible that the native
943     // layer itself implements a cache, which could result in us receiving HTTP_NOT_MODIFIED
944     // responses after all, and we should handle those correctly.
945     shouldDecodeGzip =
946         shouldDecodeGzip && connection.getResponseCode() != HttpURLConnection.HTTP_NOT_MODIFIED;
947     return new ResponseHeadersWithMetadata(
948         response.build(), shouldDecodeGzip, receivedContentLength);
949   }
950 
951   /**
952    * Receives the response body from the server and passes it to the native layer via the JNI
953    * callbacks (blocking until all response body data has been received, or an error occurs).
954    */
955   @SuppressWarnings("NonAtomicVolatileUpdate")
receiveResponseBody(HttpURLConnection connection, boolean shouldDecodeGzip)956   private void receiveResponseBody(HttpURLConnection connection, boolean shouldDecodeGzip)
957       throws IOException, AbortRequestException {
958     // Check one more time, before blocking on the InputStream, if it request has already been
959     // cancelled (to avoid starting any blocking network IO we can't easily interrupt).
960     checkClosed();
961 
962     try (CountingInputStream networkStream = getResponseBodyStream(connection);
963         InputStream inputStream = getDecodedResponseBodyStream(networkStream, shouldDecodeGzip)) {
964       long networkReceivedBytes = 0;
965       // Allocate a buffer for reading the response body data into memory and passing it to JNI.
966       int bufferSize = responseBodyChunkSizeBytes;
967       byte[] buffer = new byte[bufferSize];
968       // This outer loop runs until we reach the end of the response body stream (or hit an
969       // error).
970       int actualBytesRead = -1;
971       do {
972         int cursor = 0;
973         // Read data from the network stream (or from the decompressing input stream wrapping the
974         // network stream), filling up the buffer that we will pass to the native layer. It's likely
975         // that each read returns less data than we request. Hence, this inner loop runs until our
976         // buffer is full, the end of the data is reached, or we hit an error.
977         while (cursor < buffer.length) {
978           actualBytesRead = inputStream.read(buffer, cursor, buffer.length - cursor);
979 
980           // Update the number of received bytes (at the network level, as best as we can measure).
981           // We must do this before we break out of the loop.
982           //
983           // Note that for some implementations like Cronet's, this would count uncompressed bytes
984           // even if the original response was compressed using a Content-Encoding. Hence, this
985           // would be an over-estimate of actual network data usage. We will, however, try to
986           // provide a more accurate value once the request is completed successfully, if a
987           // Content-Length response header was available. See doOnResponseCompleted.
988           long newNetworkReceivedBytes = networkStream.getCount();
989           receivedBodyBytes += (newNetworkReceivedBytes - networkReceivedBytes);
990           networkReceivedBytes = newNetworkReceivedBytes;
991 
992           if (actualBytesRead == -1) {
993             // End of data reached (successfully). Break out of inner loop.
994             break;
995           }
996           // Some data was read.
997           cursor += actualBytesRead;
998         }
999         // If our buffer is still empty, then we must've hit the end of the data right away. No need
1000         // to call back into the native layer anymore.
1001         if (cursor == 0) {
1002           break;
1003         }
1004         // If our buffer now has some data in it, we must pass it to the native layer via the JNI
1005         // callback. This may be very fast, but may also block on disk IO and/or on-the-fly
1006         // payload decompression.
1007         doOnResponseBody(buffer, cursor);
1008 
1009         // Before trying to read another chunk of data, make sure that the request hasn't been
1010         // aborted yet.
1011         checkClosed();
1012       } while (actualBytesRead != -1);
1013     }
1014     // We're done downloading. The InputStream will be closed, letting the network layer reclaim
1015     // the socket and possibly return it to a connection pool for later reuse (as long as we don't
1016     // call #disconnect() on it, which would prevent the socket from being reused).
1017   }
1018 
1019   /** Returns the {@link java.io.InputStream} that will return the response body data. */
getResponseBodyStream(HttpURLConnection connection)1020   private static CountingInputStream getResponseBodyStream(HttpURLConnection connection)
1021       throws IOException {
1022     // If the response was an error, then we need to call getErrorStream() to get the response
1023     // body. Otherwise we need to use getInputStream().
1024     //
1025     // Note that we don't wrap the InputStream in a BufferedInputStream, since we already read data
1026     // from the unbuffered InputStream in large chunks at a time, so adding another buffering layer
1027     // in between isn't helpful.
1028     InputStream errorStream = connection.getErrorStream();
1029     if (errorStream == null) {
1030       return new CountingInputStream(connection.getInputStream());
1031     }
1032     return new CountingInputStream(errorStream);
1033   }
1034 
1035   /**
1036    * Returns an {@link java.io.InputStream} that, if we should automatically decode/decompress the
1037    * response body, will do so.
1038    *
1039    * <p>Note that if we should not automatically decode the response body, then this will simply
1040    * return {@code inputStream}.
1041    */
getDecodedResponseBodyStream( InputStream inputStream, boolean shouldDecodeGzip)1042   private InputStream getDecodedResponseBodyStream(
1043       InputStream inputStream, boolean shouldDecodeGzip) throws IOException {
1044     if (shouldDecodeGzip) {
1045       // Note that GZIPInputStream's default internal buffer size is quite small (512 bytes). We
1046       // therefore specify a buffer size explicitly, to ensure that we read in large enough chunks
1047       // from the network stream (which in turn can improve overall throughput).
1048       return new GZIPInputStream(inputStream, responseBodyGzipBufferSizeBytes);
1049     }
1050     return inputStream;
1051   }
1052 }
1053