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