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.truth.Truth.assertThat; 17 import static com.google.common.truth.Truth.assertWithMessage; 18 import static java.nio.charset.StandardCharsets.UTF_8; 19 import static org.junit.Assert.assertThrows; 20 import static org.mockito.ArgumentMatchers.any; 21 import static org.mockito.ArgumentMatchers.anyBoolean; 22 import static org.mockito.ArgumentMatchers.anyInt; 23 import static org.mockito.ArgumentMatchers.eq; 24 import static org.mockito.Mockito.doThrow; 25 import static org.mockito.Mockito.inOrder; 26 import static org.mockito.Mockito.mock; 27 import static org.mockito.Mockito.never; 28 import static org.mockito.Mockito.times; 29 import static org.mockito.Mockito.verify; 30 import static org.mockito.Mockito.when; 31 32 import com.google.common.collect.ImmutableList; 33 import com.google.common.collect.ImmutableMap; 34 import com.google.fcp.client.CallFromNativeWrapper; 35 import com.google.fcp.client.CallFromNativeWrapper.CallFromNativeRuntimeException; 36 import com.google.fcp.client.http.HttpRequestHandleImpl.HttpURLConnectionFactory; 37 import com.google.protobuf.ExtensionRegistryLite; 38 import com.google.protobuf.InvalidProtocolBufferException; 39 import com.google.rpc.Code; 40 import com.google.rpc.Status; 41 import java.io.ByteArrayInputStream; 42 import java.io.ByteArrayOutputStream; 43 import java.io.IOException; 44 import java.net.HttpURLConnection; 45 import java.net.SocketTimeoutException; 46 import java.util.LinkedHashMap; 47 import java.util.List; 48 import java.util.concurrent.ExecutorService; 49 import java.util.concurrent.Executors; 50 import java.util.zip.GZIPOutputStream; 51 import org.junit.Before; 52 import org.junit.Rule; 53 import org.junit.Test; 54 import org.junit.runner.RunWith; 55 import org.junit.runners.JUnit4; 56 import org.mockito.InOrder; 57 import org.mockito.Mock; 58 import org.mockito.junit.MockitoJUnit; 59 import org.mockito.junit.MockitoRule; 60 61 /** 62 * Unit tests for {@link HttpClientForNativeImpl}. 63 * 64 * <p>This test doesn't actually call into any native code/JNI (instead the JNI callback methods are 65 * faked out and replaced with Java-only code, which makes the code a lot easier to unit test). This 66 * test also does <strong>not</strong> exercise all of the concurrency-related edge cases that could 67 * arise (since these are difficult to conclusively test in general). 68 */ 69 @RunWith(JUnit4.class) 70 public final class HttpClientForNativeImplTest { 71 72 private static final int DEFAULT_TEST_CHUNK_BUFFER_SIZE = 5; 73 private static final double ESTIMATED_HTTP2_HEADER_COMPRESSION_RATIO = 0.5; 74 // We use an executor with real background threads, just to exercise a bit more of the code and 75 // possibly spot any concurrency issues. The use of background threads is conveniently hidden 76 // behind the performRequests interface anyway. 77 private static final ExecutorService TEST_EXECUTOR_SERVICE = Executors.newFixedThreadPool(2); 78 // Do nothing in the UncaughtExceptionHandler, letting the exception bubble up instead. 79 private static final CallFromNativeWrapper TEST_CALL_FROM_NATIVE_WRAPPER = 80 new CallFromNativeWrapper((t, e) -> {}); 81 82 /** 83 * A fake {@link HttpRequestHandleImpl} implementation which never actually calls into the native 84 * layer over JNI, and instead uses a fake pure Java implementation that emulates how the native 85 * layer would behave. This makes unit testing the Java layer possible. 86 */ 87 static class TestHttpRequestHandleImpl extends HttpRequestHandleImpl { TestHttpRequestHandleImpl( JniHttpRequest request, HttpURLConnectionFactory urlConnectionFactory, boolean supportAcceptEncodingHeader, boolean disableTimeouts)88 TestHttpRequestHandleImpl( 89 JniHttpRequest request, 90 HttpURLConnectionFactory urlConnectionFactory, 91 boolean supportAcceptEncodingHeader, 92 boolean disableTimeouts) { 93 super( 94 request, 95 TEST_CALL_FROM_NATIVE_WRAPPER, 96 TEST_EXECUTOR_SERVICE, 97 urlConnectionFactory, 98 /*connectTimeoutMs=*/ disableTimeouts ? -1 : 123, 99 /*readTimeoutMs=*/ disableTimeouts ? -1 : 456, 100 // Force the implementation to read 5 bytes at a time, to exercise the chunking logic. 101 /*requestBodyChunkSizeBytes=*/ DEFAULT_TEST_CHUNK_BUFFER_SIZE, 102 /*responseBodyChunkSizeBytes=*/ DEFAULT_TEST_CHUNK_BUFFER_SIZE, 103 /*responseBodyGzipBufferSizeBytes=*/ DEFAULT_TEST_CHUNK_BUFFER_SIZE, 104 /*callDisconnectWhenCancelled=*/ true, 105 /*supportAcceptEncodingHeader=*/ supportAcceptEncodingHeader, 106 /*estimatedHttp2HeaderCompressionRatio=*/ ESTIMATED_HTTP2_HEADER_COMPRESSION_RATIO); 107 } 108 109 // There should be no need for us to synchronize around these mutable fields, since the 110 // implementation itself should already implement the necessary synchronization to ensure that 111 // only one JNI callback method is called a time. 112 ByteArrayInputStream fakeRequestBody = null; 113 boolean readRequestBodyResult = true; 114 boolean onResponseStartedResult = true; 115 boolean onResponseBodyResult = true; 116 117 JniHttpResponse responseProto = null; 118 Status responseError = null; 119 Status responseBodyError = null; 120 ByteArrayOutputStream responseBody = new ByteArrayOutputStream(); 121 boolean completedSuccessfully = false; 122 123 @Override readRequestBody(byte[] buffer, long requestedBytes, int[] actualBytesRead)124 protected boolean readRequestBody(byte[] buffer, long requestedBytes, int[] actualBytesRead) { 125 if (!readRequestBodyResult) { 126 return false; 127 } 128 int cursor; 129 // Always return up to two bytes only. That way we ensure the implementation properly handles 130 // the case when it gets less than data back than requested. 131 for (cursor = 0; cursor < Long.min(2, requestedBytes); cursor++) { 132 int newByte = fakeRequestBody.read(); 133 if (newByte == -1) { 134 break; 135 } 136 buffer[cursor] = (byte) newByte; 137 } 138 actualBytesRead[0] = cursor == 0 ? -1 : cursor; 139 return true; 140 } 141 142 @Override onResponseStarted(byte[] responseProto)143 protected boolean onResponseStarted(byte[] responseProto) { 144 if (!onResponseStartedResult) { 145 return false; 146 } 147 try { 148 this.responseProto = 149 JniHttpResponse.parseFrom(responseProto, ExtensionRegistryLite.getEmptyRegistry()); 150 } catch (InvalidProtocolBufferException e) { 151 throw new AssertionError("invalid responseProto", e); 152 } 153 return true; 154 } 155 156 @Override onResponseError(byte[] statusProto)157 protected void onResponseError(byte[] statusProto) { 158 try { 159 responseError = Status.parseFrom(statusProto, ExtensionRegistryLite.getEmptyRegistry()); 160 } catch (InvalidProtocolBufferException e) { 161 throw new AssertionError("invalid statusProto", e); 162 } 163 } 164 165 @Override onResponseBody(byte[] data, int bytesAvailable)166 protected boolean onResponseBody(byte[] data, int bytesAvailable) { 167 if (!onResponseBodyResult) { 168 return false; 169 } 170 responseBody.write(data, 0, bytesAvailable); 171 return true; 172 } 173 174 @Override onResponseBodyError(byte[] statusProto)175 protected void onResponseBodyError(byte[] statusProto) { 176 try { 177 responseBodyError = Status.parseFrom(statusProto, ExtensionRegistryLite.getEmptyRegistry()); 178 } catch (InvalidProtocolBufferException e) { 179 throw new AssertionError("invalid statusProto", e); 180 } 181 } 182 183 @Override onResponseCompleted()184 protected void onResponseCompleted() { 185 completedSuccessfully = true; 186 } 187 188 /** 189 * Checks that the request succeeded, based on which native callback methods were/were not 190 * invoked. 191 */ assertSuccessfulCompletion()192 void assertSuccessfulCompletion() { 193 assertWithMessage("onResponseError was called").that(responseError).isNull(); 194 assertWithMessage("onResponseBodyError was called").that(responseBodyError).isNull(); 195 assertWithMessage("onResponseStarted was not called").that(responseProto).isNotNull(); 196 assertWithMessage("onResponseCompleted was not called").that(completedSuccessfully).isTrue(); 197 } 198 } 199 200 @Rule public final MockitoRule mockito = MockitoJUnit.rule(); 201 202 @Mock HttpURLConnectionFactory urlConnectionFactory; 203 204 HttpClientForNativeImpl httpClient; 205 206 @Before setUp()207 public void setUp() throws Exception { 208 httpClient = 209 new HttpClientForNativeImpl( 210 TEST_CALL_FROM_NATIVE_WRAPPER, 211 (request) -> 212 new TestHttpRequestHandleImpl( 213 request, 214 urlConnectionFactory, 215 /*supportAcceptEncodingHeader=*/ true, 216 /*disableTimeouts=*/ false)); 217 } 218 219 @Test testSingleRequestWithoutRequestBodySucceeds()220 public void testSingleRequestWithoutRequestBodySucceeds() throws Exception { 221 doTestSingleRequestWithoutRequestBodySucceeds( 222 /*supportAcceptEncodingHeader=*/ true, /*expectTimeoutsToBeSet=*/ true); 223 } 224 225 @Test testSingleRequestWithoutRequestBodyAndDisableAcceptEncodingHeaderSupportSucceeds()226 public void testSingleRequestWithoutRequestBodyAndDisableAcceptEncodingHeaderSupportSucceeds() 227 throws Exception { 228 httpClient = 229 new HttpClientForNativeImpl( 230 TEST_CALL_FROM_NATIVE_WRAPPER, 231 (request) -> 232 new TestHttpRequestHandleImpl( 233 request, 234 urlConnectionFactory, 235 /*supportAcceptEncodingHeader=*/ false, 236 /*disableTimeouts=*/ false)); 237 doTestSingleRequestWithoutRequestBodySucceeds( 238 /*supportAcceptEncodingHeader=*/ false, /*expectTimeoutsToBeSet=*/ true); 239 } 240 241 @Test testSingleRequestWithoutRequestBodyAndDisableTimeoutsSucceeds()242 public void testSingleRequestWithoutRequestBodyAndDisableTimeoutsSucceeds() throws Exception { 243 httpClient = 244 new HttpClientForNativeImpl( 245 TEST_CALL_FROM_NATIVE_WRAPPER, 246 (request) -> 247 new TestHttpRequestHandleImpl( 248 request, 249 urlConnectionFactory, 250 /*supportAcceptEncodingHeader=*/ false, 251 /*disableTimeouts=*/ true)); 252 doTestSingleRequestWithoutRequestBodySucceeds( 253 /*supportAcceptEncodingHeader=*/ false, /*expectTimeoutsToBeSet=*/ false); 254 } 255 doTestSingleRequestWithoutRequestBodySucceeds( boolean supportAcceptEncodingHeader, boolean expectTimeoutsToBeSet)256 private void doTestSingleRequestWithoutRequestBodySucceeds( 257 boolean supportAcceptEncodingHeader, boolean expectTimeoutsToBeSet) throws Exception { 258 TestHttpRequestHandleImpl requestHandle = 259 (TestHttpRequestHandleImpl) 260 httpClient.enqueueRequest( 261 JniHttpRequest.newBuilder() 262 .setUri("https://foo.com") 263 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 264 .addExtraHeaders( 265 JniHttpHeader.newBuilder() 266 .setName("Request-Header1") 267 .setValue("Foo") 268 .build()) 269 .addExtraHeaders( 270 JniHttpHeader.newBuilder() 271 .setName("Request-Header2") 272 .setValue("Bar") 273 .build()) 274 .setHasBody(false) 275 .build() 276 .toByteArray()); 277 278 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 279 when(urlConnectionFactory.createUrlConnection("https://foo.com")).thenReturn(mockConnection); 280 281 int expectedResponseCode = 200; 282 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 283 // Create a fake set of response headers. We use a LinkedHashMap rather than the less verbose 284 // ImmutableMap.of utility to allow us to add an entry for the HTTP status line, which in 285 // HttpURLConnection has a null key, which ImmutableMap disallows (to check that it gets 286 // properly handled/filtered out before passing on to JNI). Because the map still has a defined 287 // iteration order, we can still easily compare the whole response proto in one go (since we 288 // know the order the header fields will be in). 289 LinkedHashMap<String, List<String>> headerFields = new LinkedHashMap<>(); 290 headerFields.put("Response-Header1", ImmutableList.of("Bar", "Baz")); 291 headerFields.put("Response-Header2", ImmutableList.of("Barbaz")); 292 // And add a Content-Length and 'null' header (to check whether they are correctly redacted & 293 // ignored. 294 headerFields.put("Content-Length", ImmutableList.of("9999")); // Should be ignored. 295 headerFields.put(null, ImmutableList.of("200 OK")); // Should be ignored. 296 when(mockConnection.getHeaderFields()).thenReturn(headerFields); 297 298 // Fake some response body data. 299 String expectedResponseBody = "test_response_body"; 300 when(mockConnection.getInputStream()) 301 .thenReturn(new ByteArrayInputStream(expectedResponseBody.getBytes(UTF_8))); 302 303 // Run the request. 304 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 305 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 306 .isEqualTo(Code.OK_VALUE); 307 308 requestHandle.close(); 309 310 // Verify the results.. 311 requestHandle.assertSuccessfulCompletion(); 312 assertThat(requestHandle.responseProto) 313 .isEqualTo( 314 JniHttpResponse.newBuilder() 315 .setCode(expectedResponseCode) 316 // The Content-Length and 'null' headers should have been redacted. 317 .addHeaders( 318 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 319 .addHeaders( 320 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Baz").build()) 321 .addHeaders( 322 JniHttpHeader.newBuilder() 323 .setName("Response-Header2") 324 .setValue("Barbaz") 325 .build()) 326 .build()); 327 328 assertThat(requestHandle.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody); 329 330 // Verify various important request properties. 331 verify(mockConnection).setRequestMethod("GET"); 332 InOrder requestHeadersOrder = inOrder(mockConnection); 333 requestHeadersOrder.verify(mockConnection).addRequestProperty("Request-Header1", "Foo"); 334 requestHeadersOrder.verify(mockConnection).addRequestProperty("Request-Header2", "Bar"); 335 verify(mockConnection, supportAcceptEncodingHeader ? times(1) : never()) 336 .setRequestProperty("Accept-Encoding", "gzip"); 337 verify(mockConnection, expectTimeoutsToBeSet ? times(1) : never()).setConnectTimeout(123); 338 verify(mockConnection, expectTimeoutsToBeSet ? times(1) : never()).setReadTimeout(456); 339 verify(mockConnection, never()).setDoOutput(anyBoolean()); 340 verify(mockConnection, never()).getOutputStream(); 341 verify(mockConnection).setDoInput(true); 342 verify(mockConnection).setUseCaches(false); 343 verify(mockConnection).setInstanceFollowRedirects(true); 344 } 345 346 @Test testSingleRequestWithRequestBodySucceeds()347 public void testSingleRequestWithRequestBodySucceeds() throws Exception { 348 TestHttpRequestHandleImpl requestHandle = 349 (TestHttpRequestHandleImpl) 350 httpClient.enqueueRequest( 351 JniHttpRequest.newBuilder() 352 .setUri("https://foo.com") 353 .setMethod(JniHttpMethod.HTTP_METHOD_POST) 354 .addExtraHeaders( 355 JniHttpHeader.newBuilder() 356 .setName("Request-Header1") 357 .setValue("Foo") 358 .build()) 359 .addExtraHeaders( 360 JniHttpHeader.newBuilder() 361 .setName("Request-Header1") 362 .setValue("Foobar") 363 .build()) 364 .addExtraHeaders( 365 JniHttpHeader.newBuilder() 366 .setName("Request-Header2") 367 .setValue("Bar") 368 .build()) 369 .setHasBody(true) 370 .build() 371 .toByteArray()); 372 373 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 374 when(urlConnectionFactory.createUrlConnection("https://foo.com")).thenReturn(mockConnection); 375 376 // Gather request body data sent to the HttpURLConnection in a stream we can inspect later on. 377 ByteArrayOutputStream actualRequestBody = new ByteArrayOutputStream(); 378 when(mockConnection.getOutputStream()).thenReturn(actualRequestBody); 379 380 // Fake some request body data. 381 String expectedRequestBody = "test_request_body"; 382 requestHandle.fakeRequestBody = new ByteArrayInputStream(expectedRequestBody.getBytes(UTF_8)); 383 384 int expectedResponseCode = 200; 385 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 386 LinkedHashMap<String, List<String>> headerFields = new LinkedHashMap<>(); 387 headerFields.put("Response-Header1", ImmutableList.of("Bar", "Baz")); 388 headerFields.put("Response-Header2", ImmutableList.of("Barbaz")); 389 headerFields.put(null, ImmutableList.of("HTTP/1.1 200 OK")); // Should be ignored. 390 when(mockConnection.getHeaderFields()).thenReturn(headerFields); 391 392 // Add the response message ("OK"), so that it gets included in the received bytes stats. 393 when(mockConnection.getResponseMessage()).thenReturn("OK"); 394 395 // Fake some response body data. 396 String expectedResponseBody = "test_response_body"; 397 when(mockConnection.getInputStream()) 398 .thenReturn(new ByteArrayInputStream(expectedResponseBody.getBytes(UTF_8))); 399 400 // Run the request. 401 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 402 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 403 .isEqualTo(Code.OK_VALUE); 404 405 requestHandle.close(); 406 407 // Verify the results. 408 requestHandle.assertSuccessfulCompletion(); 409 assertThat(requestHandle.responseProto) 410 .isEqualTo( 411 JniHttpResponse.newBuilder() 412 .setCode(expectedResponseCode) 413 .addHeaders( 414 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 415 .addHeaders( 416 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Baz").build()) 417 .addHeaders( 418 JniHttpHeader.newBuilder() 419 .setName("Response-Header2") 420 .setValue("Barbaz") 421 .build()) 422 .build()); 423 424 assertThat(actualRequestBody.toString(UTF_8.name())).isEqualTo(expectedRequestBody); 425 assertThat(requestHandle.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody); 426 427 // Verify the network stats are accurate (they should count the request headers, URL, request 428 // method, request body, response headers and response body). 429 assertThat( 430 JniHttpSentReceivedBytes.parseFrom( 431 requestHandle.getTotalSentReceivedBytes(), 432 ExtensionRegistryLite.getEmptyRegistry())) 433 .isEqualTo( 434 JniHttpSentReceivedBytes.newBuilder() 435 .setSentBytes( 436 ("POST https://foo.com HTTP/1.1\r\n" 437 + "Request-Header1: Foo\r\n" 438 + "Request-Header1: Foobar\r\n" 439 + "Request-Header2: Bar\r\n" 440 + "\r\n") 441 .length() 442 + expectedRequestBody.length()) 443 .setReceivedBytes( 444 ("HTTP/1.1 200 OK\r\n" 445 + "Response-Header1: Bar\r\n" 446 + "Response-Header1: Baz\r\n" 447 + "Response-Header2: Barbaz\r\n" 448 + "\r\n") 449 .length() 450 + requestHandle.responseBody.size()) 451 .build()); 452 453 // Verify various important request properties. 454 verify(mockConnection).setRequestMethod("POST"); 455 InOrder requestHeadersOrder = inOrder(mockConnection); 456 requestHeadersOrder.verify(mockConnection).addRequestProperty("Request-Header1", "Foo"); 457 requestHeadersOrder.verify(mockConnection).addRequestProperty("Request-Header1", "Foobar"); 458 requestHeadersOrder.verify(mockConnection).addRequestProperty("Request-Header2", "Bar"); 459 verify(mockConnection).setConnectTimeout(123); 460 verify(mockConnection).setReadTimeout(456); 461 verify(mockConnection).setDoOutput(true); 462 // Since the request body content length wasn't known ahead of time, the 463 // 'Transfer-Encoding: chunked' streaming mode should've been enabled. 464 verify(mockConnection).setChunkedStreamingMode(5); 465 verify(mockConnection, never()).setFixedLengthStreamingMode(anyInt()); 466 verify(mockConnection).setDoInput(true); 467 verify(mockConnection).setUseCaches(false); 468 verify(mockConnection).setInstanceFollowRedirects(true); 469 } 470 471 /** 472 * Tests whether a single request with a <strong>known-ahead-of-time</strong> request body content 473 * length is processed correctly. 474 */ 475 @Test testSingleRequestWithKnownRequestContentLengthSucceeds()476 public void testSingleRequestWithKnownRequestContentLengthSucceeds() throws Exception { 477 String expectedRequestBody = "another_test_request_body"; 478 String requestBodyLength = "25"; // the length of the above string. 479 long requestBodyLengthLong = 25L; 480 assertThat(expectedRequestBody).hasLength((int) requestBodyLengthLong); 481 TestHttpRequestHandleImpl requestHandle = 482 (TestHttpRequestHandleImpl) 483 httpClient.enqueueRequest( 484 JniHttpRequest.newBuilder() 485 .setUri("https://foo.com") 486 .setMethod(JniHttpMethod.HTTP_METHOD_PUT) 487 .addExtraHeaders( 488 JniHttpHeader.newBuilder() 489 .setName("Request-Header1") 490 .setValue("Foo") 491 .build()) 492 .addExtraHeaders( 493 // Add a Content-Length request header, which should result in 'fixed 494 // length' 495 // request body streaming mode. 496 JniHttpHeader.newBuilder() 497 // We purposely use a mixed-case header name to ensure header matching 498 // is 499 // case insensitive. 500 .setName("Content-length") 501 .setValue(requestBodyLength)) 502 .setHasBody(true) 503 .build() 504 .toByteArray()); 505 506 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 507 when(urlConnectionFactory.createUrlConnection("https://foo.com")).thenReturn(mockConnection); 508 509 // Gather request body data sent to the HttpURLConnection in a stream we can inspect later on. 510 ByteArrayOutputStream actualRequestBody = new ByteArrayOutputStream(); 511 when(mockConnection.getOutputStream()).thenReturn(actualRequestBody); 512 513 // Fake some request body data. 514 requestHandle.fakeRequestBody = new ByteArrayInputStream(expectedRequestBody.getBytes(UTF_8)); 515 516 int expectedResponseCode = 201; 517 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 518 519 // Fake some response body data. 520 String expectedResponseBody = "another_test_response_body"; 521 when(mockConnection.getInputStream()) 522 .thenReturn(new ByteArrayInputStream(expectedResponseBody.getBytes(UTF_8))); 523 524 // Run the request. 525 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 526 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 527 .isEqualTo(Code.OK_VALUE); 528 529 requestHandle.close(); 530 531 // Verify the results.. 532 requestHandle.assertSuccessfulCompletion(); 533 assertThat(requestHandle.responseProto) 534 .isEqualTo(JniHttpResponse.newBuilder().setCode(expectedResponseCode).build()); 535 536 assertThat(actualRequestBody.toString(UTF_8.name())).isEqualTo(expectedRequestBody); 537 assertThat(requestHandle.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody); 538 539 verify(mockConnection).setRequestMethod("PUT"); 540 verify(mockConnection).setDoOutput(true); 541 InOrder requestHeadersOrder = inOrder(mockConnection); 542 requestHeadersOrder.verify(mockConnection).addRequestProperty("Request-Header1", "Foo"); 543 requestHeadersOrder 544 .verify(mockConnection) 545 .addRequestProperty("Content-length", requestBodyLength); 546 // Since the request body content length *was* known ahead of time, the fixed length streaming 547 // mode should have been enabled. 548 verify(mockConnection).setFixedLengthStreamingMode(requestBodyLengthLong); 549 verify(mockConnection, never()).setChunkedStreamingMode(anyInt()); 550 } 551 552 /** 553 * Tests whether a single request with a request body that is smaller than our read buffer size is 554 * processed correctly. 555 */ 556 @Test testSingleRequestWithKnownRequestContentLengthThatFitsInSingleBufferSucceeds()557 public void testSingleRequestWithKnownRequestContentLengthThatFitsInSingleBufferSucceeds() 558 throws Exception { 559 String expectedRequestBody = "1234"; 560 String requestBodyLength = 561 "4"; // the length of the above string, which is smaller than the buffer size. 562 long requestBodyLengthLong = 4L; 563 assertThat(expectedRequestBody).hasLength((int) requestBodyLengthLong); 564 assertThat(requestBodyLengthLong).isLessThan(DEFAULT_TEST_CHUNK_BUFFER_SIZE); 565 TestHttpRequestHandleImpl requestHandle = 566 (TestHttpRequestHandleImpl) 567 httpClient.enqueueRequest( 568 JniHttpRequest.newBuilder() 569 .setUri("https://foo.com") 570 .setMethod(JniHttpMethod.HTTP_METHOD_PUT) 571 .addExtraHeaders( 572 // Add a Content-Length request header, which should result in 'fixed 573 // length' 574 // request body streaming mode. 575 JniHttpHeader.newBuilder() 576 // We purposely use a mixed-case header name to ensure header matching 577 // is 578 // case insensitive. 579 .setName("content-Length") 580 .setValue(requestBodyLength)) 581 .setHasBody(true) 582 .build() 583 .toByteArray()); 584 585 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 586 when(urlConnectionFactory.createUrlConnection("https://foo.com")).thenReturn(mockConnection); 587 588 // Gather request body data sent to the HttpURLConnection in a stream we can inspect later on. 589 ByteArrayOutputStream actualRequestBody = new ByteArrayOutputStream(); 590 when(mockConnection.getOutputStream()).thenReturn(actualRequestBody); 591 592 // Fake some request body data. 593 requestHandle.fakeRequestBody = new ByteArrayInputStream(expectedRequestBody.getBytes(UTF_8)); 594 595 int expectedResponseCode = 503; 596 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 597 598 // Fake some response body data (via the error stream this time). 599 String expectedResponseBody = "abc"; 600 when(mockConnection.getErrorStream()) 601 .thenReturn(new ByteArrayInputStream(expectedResponseBody.getBytes(UTF_8))); 602 603 // Run the request. 604 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 605 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 606 .isEqualTo(Code.OK_VALUE); 607 608 requestHandle.close(); 609 610 // Verify the results.. 611 requestHandle.assertSuccessfulCompletion(); 612 assertThat(requestHandle.responseProto) 613 .isEqualTo(JniHttpResponse.newBuilder().setCode(expectedResponseCode).build()); 614 615 assertThat(actualRequestBody.toString(UTF_8.name())).isEqualTo(expectedRequestBody); 616 assertThat(requestHandle.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody); 617 618 verify(mockConnection).setRequestMethod("PUT"); 619 verify(mockConnection).setDoOutput(true); 620 verify(mockConnection).addRequestProperty("content-Length", requestBodyLength); 621 // Since the request body content length *was* known ahead of time, the fixed length streaming 622 // mode should have been enabled. 623 verify(mockConnection).setFixedLengthStreamingMode(requestBodyLengthLong); 624 verify(mockConnection, never()).setChunkedStreamingMode(anyInt()); 625 } 626 627 /** Tests whether issuing multiple concurrent requests is handled correctly. */ 628 @Test testMultipleRequestsWithRequestBodiesSucceeds()629 public void testMultipleRequestsWithRequestBodiesSucceeds() throws Exception { 630 TestHttpRequestHandleImpl requestHandle1 = 631 (TestHttpRequestHandleImpl) 632 httpClient.enqueueRequest( 633 JniHttpRequest.newBuilder() 634 .setUri("https://foo.com") 635 .setMethod(JniHttpMethod.HTTP_METHOD_POST) 636 .addExtraHeaders( 637 JniHttpHeader.newBuilder() 638 .setName("Request-Header1") 639 .setValue("Foo") 640 .build()) 641 .addExtraHeaders( 642 JniHttpHeader.newBuilder() 643 .setName("Request-Header2") 644 .setValue("Bar") 645 .build()) 646 .setHasBody(true) 647 .build() 648 .toByteArray()); 649 650 TestHttpRequestHandleImpl requestHandle2 = 651 (TestHttpRequestHandleImpl) 652 httpClient.enqueueRequest( 653 JniHttpRequest.newBuilder() 654 .setUri("https://foo2.com") 655 .setMethod(JniHttpMethod.HTTP_METHOD_PATCH) 656 .setHasBody(true) 657 .build() 658 .toByteArray()); 659 660 HttpURLConnection mockConnection1 = mock(HttpURLConnection.class); 661 when(urlConnectionFactory.createUrlConnection("https://foo.com")).thenReturn(mockConnection1); 662 HttpURLConnection mockConnection2 = mock(HttpURLConnection.class); 663 when(urlConnectionFactory.createUrlConnection("https://foo2.com")).thenReturn(mockConnection2); 664 665 // Gather request body data sent to the HttpURLConnection in a stream we can inspect later on. 666 ByteArrayOutputStream actualRequestBody1 = new ByteArrayOutputStream(); 667 when(mockConnection1.getOutputStream()).thenReturn(actualRequestBody1); 668 ByteArrayOutputStream actualRequestBody2 = new ByteArrayOutputStream(); 669 when(mockConnection2.getOutputStream()).thenReturn(actualRequestBody2); 670 671 // Fake some request body data. 672 String expectedRequestBody1 = "test_request_body1"; 673 requestHandle1.fakeRequestBody = new ByteArrayInputStream(expectedRequestBody1.getBytes(UTF_8)); 674 String expectedRequestBody2 = "another_request_body2"; 675 requestHandle2.fakeRequestBody = new ByteArrayInputStream(expectedRequestBody2.getBytes(UTF_8)); 676 677 int expectedResponseCode1 = 200; 678 int expectedResponseCode2 = 300; 679 when(mockConnection1.getResponseCode()).thenReturn(expectedResponseCode1); 680 when(mockConnection2.getResponseCode()).thenReturn(expectedResponseCode2); 681 when(mockConnection1.getHeaderFields()).thenReturn(ImmutableMap.of()); 682 when(mockConnection2.getHeaderFields()) 683 .thenReturn( 684 ImmutableMap.of( 685 "Response-Header1", 686 ImmutableList.of("Bar"), 687 "Response-Header2", 688 ImmutableList.of("Barbaz"))); 689 690 // Fake some response body data. 691 String expectedResponseBody1 = "test_response_body"; 692 when(mockConnection1.getInputStream()) 693 .thenReturn(new ByteArrayInputStream(expectedResponseBody1.getBytes(UTF_8))); 694 695 String expectedResponseBody2 = "test_response_body"; 696 when(mockConnection2.getInputStream()) 697 .thenReturn(new ByteArrayInputStream(expectedResponseBody2.getBytes(UTF_8))); 698 699 // Run both requests (we provide them in opposite order to how they were created, just to try 700 // to exercise more edge conditions). 701 byte[] result = httpClient.performRequests(new Object[] {requestHandle2, requestHandle1}); 702 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 703 .isEqualTo(Code.OK_VALUE); 704 705 requestHandle1.close(); 706 requestHandle2.close(); 707 708 // Verify the results.. 709 requestHandle1.assertSuccessfulCompletion(); 710 assertThat(requestHandle1.responseProto) 711 .isEqualTo(JniHttpResponse.newBuilder().setCode(expectedResponseCode1).build()); 712 713 requestHandle2.assertSuccessfulCompletion(); 714 assertThat(requestHandle2.responseProto) 715 .isEqualTo( 716 JniHttpResponse.newBuilder() 717 .setCode(expectedResponseCode2) 718 .addHeaders( 719 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 720 .addHeaders( 721 JniHttpHeader.newBuilder() 722 .setName("Response-Header2") 723 .setValue("Barbaz") 724 .build()) 725 .build()); 726 727 assertThat(actualRequestBody1.toString(UTF_8.name())).isEqualTo(expectedRequestBody1); 728 assertThat(requestHandle1.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody1); 729 assertThat(actualRequestBody2.toString(UTF_8.name())).isEqualTo(expectedRequestBody2); 730 assertThat(requestHandle2.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody2); 731 732 // Verify various important request properties. 733 verify(mockConnection1).setRequestMethod("POST"); 734 verify(mockConnection2).setRequestMethod("PATCH"); 735 InOrder requestHeadersOrder = inOrder(mockConnection1); 736 requestHeadersOrder.verify(mockConnection1).addRequestProperty("Request-Header1", "Foo"); 737 requestHeadersOrder.verify(mockConnection1).addRequestProperty("Request-Header2", "Bar"); 738 verify(mockConnection2, never()).addRequestProperty(any(), any()); 739 } 740 741 @Test testGzipResponseBodyDecompressionSucceeds()742 public void testGzipResponseBodyDecompressionSucceeds() throws Exception { 743 TestHttpRequestHandleImpl requestHandle = 744 (TestHttpRequestHandleImpl) 745 httpClient.enqueueRequest( 746 JniHttpRequest.newBuilder() 747 .setUri("https://foo.com") 748 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 749 .addExtraHeaders( 750 JniHttpHeader.newBuilder() 751 .setName("Request-Header1") 752 .setValue("Foo") 753 .build()) 754 .setHasBody(false) 755 .build() 756 .toByteArray()); 757 758 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 759 when(urlConnectionFactory.createUrlConnection("https://foo.com")).thenReturn(mockConnection); 760 761 int expectedResponseCode = 200; 762 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 763 when(mockConnection.getResponseMessage()).thenReturn("OK"); 764 765 // Fake some response body data. 766 String expectedResponseBody = "test_response_body"; 767 ByteArrayOutputStream compressedResponseBody = new ByteArrayOutputStream(); 768 GZIPOutputStream compressedResponseBodyGzipStream = 769 new GZIPOutputStream(compressedResponseBody); 770 compressedResponseBodyGzipStream.write(expectedResponseBody.getBytes(UTF_8)); 771 compressedResponseBodyGzipStream.finish(); 772 when(mockConnection.getInputStream()) 773 .thenReturn(new ByteArrayInputStream(compressedResponseBody.toByteArray())); 774 // And add Content-Encoding and Transfer-Encoding headers (to check whether they are correctly 775 // redacted). 776 when(mockConnection.getHeaderFields()) 777 .thenReturn( 778 ImmutableMap.of( 779 "Response-Header1", 780 ImmutableList.of("Bar"), 781 "Content-Encoding", 782 ImmutableList.of("gzip"), 783 "Transfer-Encoding", 784 ImmutableList.of("chunked"))); 785 786 // Run the request. 787 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 788 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 789 .isEqualTo(Code.OK_VALUE); 790 791 requestHandle.close(); 792 793 // Verify the results.. 794 requestHandle.assertSuccessfulCompletion(); 795 assertThat(requestHandle.responseProto) 796 .isEqualTo( 797 JniHttpResponse.newBuilder() 798 .setCode(expectedResponseCode) 799 // The Content-Encoding and Transfer-Encoding headers should have been redacted. 800 .addHeaders( 801 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 802 .build()); 803 804 assertThat(requestHandle.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody); 805 806 // Verify the network stats are accurate (they should count the request headers, URL, request 807 // method, request body, response headers and *compressed* response body, since decompression 808 // was performed by us and hence we were able to observe and count the compressed bytes). 809 assertThat( 810 JniHttpSentReceivedBytes.parseFrom( 811 requestHandle.getTotalSentReceivedBytes(), 812 ExtensionRegistryLite.getEmptyRegistry())) 813 .isEqualTo( 814 JniHttpSentReceivedBytes.newBuilder() 815 .setSentBytes( 816 ("GET https://foo.com HTTP/1.1\r\n" + "Request-Header1: Foo\r\n" + "\r\n") 817 .length()) 818 .setReceivedBytes( 819 ("HTTP/1.1 200 OK\r\n" 820 + "Response-Header1: Bar\r\n" 821 + "Content-Encoding: gzip\r\n" 822 + "Transfer-Encoding: chunked\r\n" 823 + "\r\n") 824 .length() 825 + compressedResponseBody.size()) 826 .build()); 827 828 // Verify various important request properties. 829 verify(mockConnection).setRequestMethod("GET"); 830 verify(mockConnection).addRequestProperty("Request-Header1", "Foo"); 831 verify(mockConnection).setRequestProperty("Accept-Encoding", "gzip"); 832 } 833 834 @Test testGzipResponseBodyWithAcceptEncodingRequestHeaderShouldNotAutoDecompress()835 public void testGzipResponseBodyWithAcceptEncodingRequestHeaderShouldNotAutoDecompress() 836 throws Exception { 837 TestHttpRequestHandleImpl requestHandle = 838 (TestHttpRequestHandleImpl) 839 httpClient.enqueueRequest( 840 JniHttpRequest.newBuilder() 841 .setUri("https://foo.com") 842 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 843 .addExtraHeaders( 844 JniHttpHeader.newBuilder() 845 .setName("Request-Header1") 846 .setValue("Foo") 847 .build()) 848 .addExtraHeaders( 849 // We purposely use mixed-case, to ensure case-insensitive matching is used. 850 JniHttpHeader.newBuilder() 851 .setName("Accept-encoding") 852 .setValue("gzip,foobar") 853 .build()) 854 .setHasBody(false) 855 .build() 856 .toByteArray()); 857 858 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 859 when(urlConnectionFactory.createUrlConnection("https://foo.com")).thenReturn(mockConnection); 860 861 int expectedResponseCode = 200; 862 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 863 864 // Fake some response body data. 865 String expectedResponseBody = "i_should_not_be_decompressed"; 866 when(mockConnection.getInputStream()) 867 .thenReturn(new ByteArrayInputStream(expectedResponseBody.getBytes(UTF_8))); 868 // And add Content-Encoding and Content-Length headers (to check whether the first header is 869 // correctly left *un*redacted, and the second is still redacted). 870 when(mockConnection.getHeaderFields()) 871 .thenReturn( 872 ImmutableMap.of( 873 "Response-Header1", 874 ImmutableList.of("Bar"), 875 "Content-Encoding", 876 ImmutableList.of("gzip"), 877 "Content-Length", 878 ImmutableList.of("9999"))); 879 880 // Run the request. 881 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 882 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 883 .isEqualTo(Code.OK_VALUE); 884 885 requestHandle.close(); 886 887 // Verify the results.. 888 requestHandle.assertSuccessfulCompletion(); 889 assertThat(requestHandle.responseProto) 890 .isEqualTo( 891 JniHttpResponse.newBuilder() 892 .setCode(expectedResponseCode) 893 // The Content-Length header should have been redacted. 894 .addHeaders( 895 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 896 .addHeaders( 897 JniHttpHeader.newBuilder().setName("Content-Encoding").setValue("gzip").build()) 898 .build()); 899 900 // The response body should have been returned without trying to decompress it. 901 assertThat(requestHandle.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody); 902 903 // Verify various important request properties. 904 verify(mockConnection).setRequestMethod("GET"); 905 verify(mockConnection).addRequestProperty("Request-Header1", "Foo"); 906 // The Accept-Encoding header provided by the native layer should have been used, verbatim. 907 verify(mockConnection).addRequestProperty("Accept-encoding", "gzip,foobar"); 908 verify(mockConnection, never()).setRequestProperty(eq("Accept-Encoding"), any()); 909 } 910 911 @Test testChunkedTransferEncodingResponseHeaderShouldBeRemoved()912 public void testChunkedTransferEncodingResponseHeaderShouldBeRemoved() throws Exception { 913 TestHttpRequestHandleImpl requestHandle = 914 (TestHttpRequestHandleImpl) 915 httpClient.enqueueRequest( 916 JniHttpRequest.newBuilder() 917 .setUri("https://foo.com") 918 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 919 .setHasBody(false) 920 .build() 921 .toByteArray()); 922 923 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 924 when(urlConnectionFactory.createUrlConnection("https://foo.com")).thenReturn(mockConnection); 925 926 int expectedResponseCode = 200; 927 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 928 929 // Fake some response body data. 930 String expectedResponseBody = "another_test_response_body"; 931 when(mockConnection.getInputStream()) 932 .thenReturn(new ByteArrayInputStream(expectedResponseBody.getBytes(UTF_8))); 933 934 // And make the response headers include a "Transfer-Encoding: chunked" header, simulating the 935 // case when HttpClientForNativeImpl is used with the JDK, which will un-chunk response data but 936 // which will not remove the Transfer-Encoding header afterwards (contrary to Android's 937 // HttpURLConnection implementation which *does* remove the header in this case). 938 when(mockConnection.getHeaderFields()) 939 .thenReturn( 940 ImmutableMap.of( 941 "Response-Header1", 942 ImmutableList.of("Bar"), 943 "Transfer-Encoding", 944 ImmutableList.of("chunked"))); 945 // Make the response body length *not* be known ahead of time (in accordance with the "chunked" 946 // transfer encoding having been used. 947 when(mockConnection.getContentLength()).thenReturn(-1); 948 949 // Run the request. 950 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 951 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 952 .isEqualTo(Code.OK_VALUE); 953 954 requestHandle.close(); 955 956 // Verify the results. 957 requestHandle.assertSuccessfulCompletion(); 958 assertThat(requestHandle.responseProto) 959 .isEqualTo( 960 JniHttpResponse.newBuilder() 961 .setCode(expectedResponseCode) 962 // The Transfer-Encoding header should have been redacted. 963 .addHeaders( 964 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 965 .build()); 966 assertThat(requestHandle.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody); 967 } 968 969 @Test testContentLengthResponseHeaderShouldDetermineReceivedBytesEstimate()970 public void testContentLengthResponseHeaderShouldDetermineReceivedBytesEstimate() 971 throws Exception { 972 TestHttpRequestHandleImpl requestHandle = 973 (TestHttpRequestHandleImpl) 974 httpClient.enqueueRequest( 975 JniHttpRequest.newBuilder() 976 .setUri("https://foo.com") 977 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 978 .setHasBody(false) 979 .build() 980 .toByteArray()); 981 982 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 983 when(urlConnectionFactory.createUrlConnection("https://foo.com")).thenReturn(mockConnection); 984 985 int expectedResponseCode = 200; 986 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 987 when(mockConnection.getResponseMessage()).thenReturn("OK"); 988 989 // Fake some response body data. 990 String expectedResponseBody = "another_test_response_body"; 991 when(mockConnection.getInputStream()) 992 .thenReturn(new ByteArrayInputStream(expectedResponseBody.getBytes(UTF_8))); 993 994 // And make the response headers include a "Content-Length" header. The header should be ignored 995 // for the most part, *but* it should be used to produce the final estimated 'received bytes' 996 // statistic, if the request completes successfully. 997 int expectedContentLength = 5; 998 when(mockConnection.getHeaderFields()) 999 .thenReturn( 1000 ImmutableMap.of( 1001 "Response-Header1", 1002 ImmutableList.of("Bar"), 1003 // Simulate a Content-Length header that has value that is smaller than the length 1004 // of the response body we actually observe (e.g. a Cronet-based implementation has 1005 // decompressed the content for us, but still told us the original length). 1006 "Content-Length", 1007 ImmutableList.of(Integer.toString(expectedContentLength)))); 1008 1009 // Run the request. 1010 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1011 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1012 .isEqualTo(Code.OK_VALUE); 1013 1014 requestHandle.close(); 1015 1016 // Verify the results. 1017 requestHandle.assertSuccessfulCompletion(); 1018 assertThat(requestHandle.responseProto) 1019 .isEqualTo( 1020 JniHttpResponse.newBuilder() 1021 .setCode(expectedResponseCode) 1022 // The Content-Length header should have been redacted. 1023 .addHeaders( 1024 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 1025 .build()); 1026 assertThat(requestHandle.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody); 1027 1028 // Verify the network stats are accurate (they should count the request headers, URL, request 1029 // method, request body, response headers and the *content length* rather than the observed 1030 // response body). 1031 assertThat( 1032 JniHttpSentReceivedBytes.parseFrom( 1033 requestHandle.getTotalSentReceivedBytes(), 1034 ExtensionRegistryLite.getEmptyRegistry())) 1035 .isEqualTo( 1036 JniHttpSentReceivedBytes.newBuilder() 1037 .setSentBytes("GET https://foo.com HTTP/1.1\r\n\r\n".length()) 1038 .setReceivedBytes( 1039 ("HTTP/1.1 200 OK\r\n" 1040 + "Response-Header1: Bar\r\n" 1041 + "Content-Length: 5\r\n" 1042 + "\r\n") 1043 .length() 1044 + expectedContentLength) 1045 .build()); 1046 } 1047 1048 @Test testHttp2RequestsShouldUseEstimatedHeaderCompressionRatio()1049 public void testHttp2RequestsShouldUseEstimatedHeaderCompressionRatio() throws Exception { 1050 TestHttpRequestHandleImpl requestHandle = 1051 (TestHttpRequestHandleImpl) 1052 httpClient.enqueueRequest( 1053 JniHttpRequest.newBuilder() 1054 .setUri("https://foo.com") 1055 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1056 .setHasBody(false) 1057 .build() 1058 .toByteArray()); 1059 1060 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1061 when(urlConnectionFactory.createUrlConnection("https://foo.com")).thenReturn(mockConnection); 1062 1063 int expectedResponseCode = 200; 1064 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 1065 // Return an empty response message, which is the heuristic that indicates HTTP/2 was likely 1066 // used to service the request. 1067 when(mockConnection.getResponseMessage()).thenReturn(""); 1068 1069 // Fake some response body data. 1070 String expectedResponseBody = "another_test_response_body"; 1071 when(mockConnection.getInputStream()) 1072 .thenReturn(new ByteArrayInputStream(expectedResponseBody.getBytes(UTF_8))); 1073 1074 // And make the response headers include a "Content-Length" header. The header should be ignored 1075 // for the most part, *but* it should be used to produce the final estimated 'received bytes' 1076 // statistic, if the request completes successfully. 1077 when(mockConnection.getHeaderFields()) 1078 .thenReturn(ImmutableMap.of("Response-Header1", ImmutableList.of("Bar"))); 1079 1080 // Run the request. 1081 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1082 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1083 .isEqualTo(Code.OK_VALUE); 1084 1085 requestHandle.close(); 1086 1087 // Verify the results. 1088 requestHandle.assertSuccessfulCompletion(); 1089 assertThat(requestHandle.responseProto) 1090 .isEqualTo( 1091 JniHttpResponse.newBuilder() 1092 .setCode(expectedResponseCode) 1093 .addHeaders( 1094 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 1095 .build()); 1096 assertThat(requestHandle.responseBody.toString(UTF_8.name())).isEqualTo(expectedResponseBody); 1097 1098 // Verify the network stats are accurate (they should count the request headers, URL, request 1099 // method, request body, response headers and the response body). Since HTTP/2 was used 1100 // (according to the heuristic), the request/response headers should have a compression factor 1101 // applied to them. 1102 assertThat( 1103 JniHttpSentReceivedBytes.parseFrom( 1104 requestHandle.getTotalSentReceivedBytes(), 1105 ExtensionRegistryLite.getEmptyRegistry())) 1106 .isEqualTo( 1107 JniHttpSentReceivedBytes.newBuilder() 1108 // Even though HTTP/2 was used, our sent/received bytes estimates hardcode an 1109 // assumption that HTTP/1.1-style status lines and CRLF-terminated headers were sent 1110 // received (and then simply applies a compression factor over the length of those 1111 // strings). 1112 .setSentBytes( 1113 (long) 1114 ("GET https://foo.com HTTP/1.1\r\n\r\n".length() 1115 * ESTIMATED_HTTP2_HEADER_COMPRESSION_RATIO)) 1116 .setReceivedBytes( 1117 (long) 1118 (("HTTP/1.1 200 \r\n" + "Response-Header1: Bar\r\n" + "\r\n").length() 1119 * ESTIMATED_HTTP2_HEADER_COMPRESSION_RATIO) 1120 + expectedResponseBody.length()) 1121 .build()); 1122 } 1123 1124 @Test testPerformOnClosedRequestShouldThrow()1125 public void testPerformOnClosedRequestShouldThrow() throws Exception { 1126 TestHttpRequestHandleImpl requestHandle = 1127 (TestHttpRequestHandleImpl) 1128 httpClient.enqueueRequest( 1129 JniHttpRequest.newBuilder() 1130 .setUri("https://foo.com") 1131 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1132 .build() 1133 .toByteArray()); 1134 1135 // Close the request before we issue it. 1136 requestHandle.close(); 1137 1138 // Since performRequests wasn't called yet, no callbacks should've been invoked as a result of 1139 // the call to close(). 1140 assertThat(requestHandle.responseError).isNull(); 1141 assertThat(requestHandle.responseProto).isNull(); 1142 assertThat(requestHandle.responseBodyError).isNull(); 1143 assertThat(requestHandle.completedSuccessfully).isFalse(); 1144 1145 // Try to perform the request, it should fail. 1146 CallFromNativeRuntimeException thrown = 1147 assertThrows( 1148 CallFromNativeRuntimeException.class, 1149 () -> httpClient.performRequests(new Object[] {requestHandle})); 1150 assertThat(thrown).hasCauseThat().isInstanceOf(IllegalStateException.class); 1151 } 1152 1153 @Test testRequestWithAcceptEncodingHeaderIfNotSupportedShouldResultInError()1154 public void testRequestWithAcceptEncodingHeaderIfNotSupportedShouldResultInError() 1155 throws Exception { 1156 // Disable support for the Accept-Encoding header. 1157 httpClient = 1158 new HttpClientForNativeImpl( 1159 TEST_CALL_FROM_NATIVE_WRAPPER, 1160 (request) -> 1161 new TestHttpRequestHandleImpl( 1162 request, 1163 urlConnectionFactory, 1164 /*supportAcceptEncodingHeader=*/ false, 1165 /*disableTimeouts=*/ false)); 1166 1167 TestHttpRequestHandleImpl requestHandle = 1168 (TestHttpRequestHandleImpl) 1169 httpClient.enqueueRequest( 1170 JniHttpRequest.newBuilder() 1171 .setUri("https://foo.com") 1172 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1173 .addExtraHeaders( 1174 JniHttpHeader.newBuilder().setName("Content-Length").setValue("1")) 1175 .addExtraHeaders( 1176 JniHttpHeader.newBuilder().setName("Accept-Encoding").setValue("gzip")) 1177 .setHasBody(false) 1178 .build() 1179 .toByteArray()); 1180 1181 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1182 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1183 1184 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1185 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1186 .isEqualTo(Code.OK_VALUE); 1187 1188 assertThat(requestHandle.responseError).isNotNull(); 1189 assertThat(requestHandle.responseError.getCode()).isEqualTo(Code.INVALID_ARGUMENT_VALUE); 1190 assertThat(requestHandle.responseProto).isNull(); 1191 assertThat(requestHandle.responseBodyError).isNull(); 1192 assertThat(requestHandle.completedSuccessfully).isFalse(); 1193 } 1194 1195 @Test testNoBodyButHasRequestContentLengthShouldResultInError()1196 public void testNoBodyButHasRequestContentLengthShouldResultInError() throws Exception { 1197 TestHttpRequestHandleImpl requestHandle = 1198 (TestHttpRequestHandleImpl) 1199 httpClient.enqueueRequest( 1200 JniHttpRequest.newBuilder() 1201 .setUri("https://foo.com") 1202 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1203 .addExtraHeaders( 1204 JniHttpHeader.newBuilder().setName("Content-Length").setValue("1").build()) 1205 .setHasBody(false) 1206 .build() 1207 .toByteArray()); 1208 1209 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1210 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1211 1212 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1213 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1214 .isEqualTo(Code.OK_VALUE); 1215 1216 assertThat(requestHandle.responseError).isNotNull(); 1217 assertThat(requestHandle.responseError.getCode()).isEqualTo(Code.INVALID_ARGUMENT_VALUE); 1218 assertThat(requestHandle.responseProto).isNull(); 1219 assertThat(requestHandle.responseBodyError).isNull(); 1220 assertThat(requestHandle.completedSuccessfully).isFalse(); 1221 } 1222 1223 /** 1224 * If something about the network OutputStream throws an exception during request body upload, 1225 * then we should return an error to native. 1226 */ 1227 @Test testSendRequestBodyExceptionShouldResultInResponseError()1228 public void testSendRequestBodyExceptionShouldResultInResponseError() throws Exception { 1229 TestHttpRequestHandleImpl requestHandle = 1230 (TestHttpRequestHandleImpl) 1231 httpClient.enqueueRequest( 1232 JniHttpRequest.newBuilder() 1233 .setUri("https://foo.com") 1234 .setMethod(JniHttpMethod.HTTP_METHOD_POST) 1235 .setHasBody(true) 1236 .build() 1237 .toByteArray()); 1238 1239 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1240 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1241 1242 when(mockConnection.getOutputStream()).thenThrow(new IOException("my error")); 1243 1244 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1245 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1246 .isEqualTo(Code.OK_VALUE); 1247 1248 assertThat(requestHandle.responseError).isNotNull(); 1249 assertThat(requestHandle.responseError.getCode()).isEqualTo(Code.UNAVAILABLE_VALUE); 1250 assertThat(requestHandle.responseError.getMessage()).contains("IOException"); 1251 assertThat(requestHandle.responseError.getMessage()).contains("my error"); 1252 assertThat(requestHandle.responseProto).isNull(); 1253 assertThat(requestHandle.responseBodyError).isNull(); 1254 assertThat(requestHandle.completedSuccessfully).isFalse(); 1255 } 1256 1257 /** 1258 * If the request got cancelled during request body upload, then we should return a CANCELLED 1259 * error to native. 1260 */ 1261 @Test testCancellationDuringSendRequestBodyShouldResultInResponseError()1262 public void testCancellationDuringSendRequestBodyShouldResultInResponseError() throws Exception { 1263 TestHttpRequestHandleImpl requestHandle = 1264 (TestHttpRequestHandleImpl) 1265 httpClient.enqueueRequest( 1266 JniHttpRequest.newBuilder() 1267 .setUri("https://foo.com") 1268 .setMethod(JniHttpMethod.HTTP_METHOD_POST) 1269 .setHasBody(true) 1270 .build() 1271 .toByteArray()); 1272 1273 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1274 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1275 1276 when(mockConnection.getOutputStream()) 1277 .thenAnswer( 1278 invocation -> { 1279 // Trigger the request cancellationF 1280 requestHandle.close(); 1281 return new ByteArrayOutputStream(); 1282 }); 1283 1284 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1285 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1286 .isEqualTo(Code.OK_VALUE); 1287 1288 assertThat(requestHandle.responseError).isNotNull(); 1289 assertThat(requestHandle.responseError.getCode()).isEqualTo(Code.CANCELLED_VALUE); 1290 assertThat(requestHandle.responseProto).isNull(); 1291 assertThat(requestHandle.responseBodyError).isNull(); 1292 assertThat(requestHandle.completedSuccessfully).isFalse(); 1293 } 1294 1295 /** 1296 * If something fails when reading request body data from JNI, then we should *not* call any more 1297 * JNI callbacks again, since the native layer will already have handled the error. 1298 */ 1299 @Test testReadRequestBodyFromNativeFailureShouldNotCallJNICallback()1300 public void testReadRequestBodyFromNativeFailureShouldNotCallJNICallback() throws Exception { 1301 TestHttpRequestHandleImpl requestHandle = 1302 (TestHttpRequestHandleImpl) 1303 httpClient.enqueueRequest( 1304 JniHttpRequest.newBuilder() 1305 .setUri("https://foo.com") 1306 .setMethod(JniHttpMethod.HTTP_METHOD_POST) 1307 .setHasBody(true) 1308 .build() 1309 .toByteArray()); 1310 1311 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1312 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1313 1314 when(mockConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); 1315 // Make the fake readRequestBody JNI method return an error. 1316 requestHandle.readRequestBodyResult = false; 1317 1318 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1319 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1320 .isEqualTo(Code.OK_VALUE); 1321 1322 // No callbacks should have been invoked. 1323 assertThat(requestHandle.responseError).isNull(); 1324 assertThat(requestHandle.responseProto).isNull(); 1325 assertThat(requestHandle.responseBodyError).isNull(); 1326 assertThat(requestHandle.completedSuccessfully).isFalse(); 1327 } 1328 1329 /** If establishing the connections fails, then we should return an error to native. */ 1330 @Test testConnectExceptionShouldResultInResponseError()1331 public void testConnectExceptionShouldResultInResponseError() throws Exception { 1332 TestHttpRequestHandleImpl requestHandle = 1333 (TestHttpRequestHandleImpl) 1334 httpClient.enqueueRequest( 1335 JniHttpRequest.newBuilder() 1336 .setUri("https://foo.com") 1337 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1338 .setHasBody(false) 1339 .build() 1340 .toByteArray()); 1341 1342 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1343 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1344 1345 doThrow(new IOException("my error")).when(mockConnection).connect(); 1346 1347 // Run the request. 1348 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1349 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1350 .isEqualTo(Code.OK_VALUE); 1351 1352 requestHandle.close(); 1353 1354 assertThat(requestHandle.responseProto).isNull(); 1355 assertThat(requestHandle.responseError).isNotNull(); 1356 assertThat(requestHandle.responseError.getCode()).isEqualTo(Code.UNAVAILABLE_VALUE); 1357 assertThat(requestHandle.responseError.getMessage()).contains("IOException"); 1358 assertThat(requestHandle.responseError.getMessage()).contains("my error"); 1359 assertThat(requestHandle.responseBodyError).isNull(); 1360 assertThat(requestHandle.completedSuccessfully).isFalse(); 1361 1362 // Verify that the request headers are counted in the network stats, since we can't really know 1363 // whether the connect() method failed before any network connection was established, or whether 1364 // it failed after we did already send our request onto the wire (each HttpURLConnection 1365 // implementation can have slightly different behavior in this regard). 1366 assertThat( 1367 JniHttpSentReceivedBytes.parseFrom( 1368 requestHandle.getTotalSentReceivedBytes(), 1369 ExtensionRegistryLite.getEmptyRegistry())) 1370 .isEqualTo( 1371 JniHttpSentReceivedBytes.newBuilder() 1372 .setSentBytes("GET https://foo.com HTTP/1.1\r\n\r\n".length()) 1373 .setReceivedBytes(0) 1374 .build()); 1375 } 1376 1377 /** 1378 * If something about the network InputStream throws an exception during response headers 1379 * receiving, then we should return an error to native. 1380 */ 1381 @Test testReceiveResponseHeadersExceptionShouldResultInResponseError()1382 public void testReceiveResponseHeadersExceptionShouldResultInResponseError() throws Exception { 1383 TestHttpRequestHandleImpl requestHandle = 1384 (TestHttpRequestHandleImpl) 1385 httpClient.enqueueRequest( 1386 JniHttpRequest.newBuilder() 1387 .setUri("https://foo.com") 1388 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1389 .setHasBody(false) 1390 .build() 1391 .toByteArray()); 1392 1393 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1394 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1395 1396 when(mockConnection.getResponseCode()).thenThrow(new IOException("my error")); 1397 1398 // Run the request. 1399 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1400 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1401 .isEqualTo(Code.OK_VALUE); 1402 1403 requestHandle.close(); 1404 1405 assertThat(requestHandle.responseProto).isNull(); 1406 assertThat(requestHandle.responseError).isNotNull(); 1407 assertThat(requestHandle.responseError.getCode()).isEqualTo(Code.UNAVAILABLE_VALUE); 1408 assertThat(requestHandle.responseError.getMessage()).contains("IOException"); 1409 assertThat(requestHandle.responseError.getMessage()).contains("my error"); 1410 assertThat(requestHandle.responseBodyError).isNull(); 1411 assertThat(requestHandle.completedSuccessfully).isFalse(); 1412 } 1413 1414 /** 1415 * If something about the network InputStream throws a {@link java.net.SocketTimeoutException} 1416 * during response headers receiving, then we should return a specific DEADLINE_EXCEEDED error to 1417 * native. 1418 */ 1419 @Test testReceiveResponseHeadersTimeoutExceptionShouldResultInResponseError()1420 public void testReceiveResponseHeadersTimeoutExceptionShouldResultInResponseError() 1421 throws Exception { 1422 TestHttpRequestHandleImpl requestHandle = 1423 (TestHttpRequestHandleImpl) 1424 httpClient.enqueueRequest( 1425 JniHttpRequest.newBuilder() 1426 .setUri("https://foo.com") 1427 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1428 .setHasBody(false) 1429 .build() 1430 .toByteArray()); 1431 1432 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1433 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1434 1435 when(mockConnection.getResponseCode()).thenThrow(new SocketTimeoutException("my error")); 1436 1437 // Run the request. 1438 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1439 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1440 .isEqualTo(Code.OK_VALUE); 1441 1442 requestHandle.close(); 1443 1444 assertThat(requestHandle.responseProto).isNull(); 1445 assertThat(requestHandle.responseError.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED_VALUE); 1446 assertThat(requestHandle.responseError.getMessage()).contains("SocketTimeoutException"); 1447 assertThat(requestHandle.responseError.getMessage()).contains("my error"); 1448 assertThat(requestHandle.responseBodyError).isNull(); 1449 assertThat(requestHandle.completedSuccessfully).isFalse(); 1450 } 1451 1452 /** 1453 * If the request gets cancelled during response headers receiving, then we should return a 1454 * CANCELLED error to native. 1455 */ 1456 @Test testCancellationDuringReceiveResponseHeadersShouldResultInResponseError()1457 public void testCancellationDuringReceiveResponseHeadersShouldResultInResponseError() 1458 throws Exception { 1459 TestHttpRequestHandleImpl requestHandle = 1460 (TestHttpRequestHandleImpl) 1461 httpClient.enqueueRequest( 1462 JniHttpRequest.newBuilder() 1463 .setUri("https://foo.com") 1464 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1465 .setHasBody(false) 1466 .build() 1467 .toByteArray()); 1468 1469 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1470 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1471 1472 when(mockConnection.getResponseCode()) 1473 .thenAnswer( 1474 invocation -> { 1475 // Trigger a cancellation of the request. 1476 requestHandle.close(); 1477 return 200; 1478 }); 1479 1480 // Run the request. 1481 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1482 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1483 .isEqualTo(Code.OK_VALUE); 1484 1485 requestHandle.close(); 1486 1487 assertThat(requestHandle.responseProto).isNull(); 1488 assertThat(requestHandle.responseError.getCode()).isEqualTo(Code.CANCELLED_VALUE); 1489 assertThat(requestHandle.responseBodyError).isNull(); 1490 assertThat(requestHandle.completedSuccessfully).isFalse(); 1491 } 1492 1493 /** 1494 * If something fails when writing response header data to JNI, then we should *not* call any more 1495 * JNI callbacks again, since the native layer will already have handled the error. 1496 */ 1497 @Test testWriteResponseHeadersToNativeFailureShouldNotCallJNICallback()1498 public void testWriteResponseHeadersToNativeFailureShouldNotCallJNICallback() throws Exception { 1499 TestHttpRequestHandleImpl requestHandle = 1500 (TestHttpRequestHandleImpl) 1501 httpClient.enqueueRequest( 1502 JniHttpRequest.newBuilder() 1503 .setUri("https://foo.com") 1504 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1505 .setHasBody(false) 1506 .build() 1507 .toByteArray()); 1508 1509 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1510 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1511 1512 int expectedResponseCode = 300; 1513 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 1514 // Make the onResponseStarted JNI method fail when it receives the data. 1515 requestHandle.onResponseStartedResult = false; 1516 1517 // Run the request. 1518 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1519 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1520 .isEqualTo(Code.OK_VALUE); 1521 1522 requestHandle.close(); 1523 1524 // No callbacks should have been invoked. 1525 assertThat(requestHandle.responseError).isNull(); 1526 assertThat(requestHandle.responseProto).isNull(); 1527 assertThat(requestHandle.responseBodyError).isNull(); 1528 assertThat(requestHandle.completedSuccessfully).isFalse(); 1529 } 1530 1531 /** 1532 * If something about the network InputStream throws an exception during response body download, 1533 * then we should return an error to native. 1534 */ 1535 @Test testReceiveResponseBodyExceptionShouldResultInResponseBodyError()1536 public void testReceiveResponseBodyExceptionShouldResultInResponseBodyError() throws Exception { 1537 TestHttpRequestHandleImpl requestHandle = 1538 (TestHttpRequestHandleImpl) 1539 httpClient.enqueueRequest( 1540 JniHttpRequest.newBuilder() 1541 .setUri("https://foo.com") 1542 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1543 .setHasBody(false) 1544 .build() 1545 .toByteArray()); 1546 1547 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1548 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1549 1550 int expectedResponseCode = 300; 1551 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 1552 when(mockConnection.getHeaderFields()) 1553 .thenReturn(ImmutableMap.of("Response-Header1", ImmutableList.of("Bar"))); 1554 1555 // Make the response body input stream throw an exception. 1556 when(mockConnection.getInputStream()).thenThrow(new IOException("my error")); 1557 when(mockConnection.getContentLength()).thenReturn(-1); 1558 1559 // Run the request. 1560 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1561 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1562 .isEqualTo(Code.OK_VALUE); 1563 1564 requestHandle.close(); 1565 1566 // Despite having hit an IOException during the response body download, we should still first 1567 // have passed the response headers to native. 1568 assertThat(requestHandle.responseProto) 1569 .isEqualTo( 1570 JniHttpResponse.newBuilder() 1571 .setCode(300) 1572 .addHeaders( 1573 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 1574 .build()); 1575 assertThat(requestHandle.responseError).isNull(); 1576 assertThat(requestHandle.responseBodyError).isNotNull(); 1577 assertThat(requestHandle.responseBodyError.getCode()).isEqualTo(Code.UNAVAILABLE_VALUE); 1578 assertThat(requestHandle.responseBodyError.getMessage()).contains("IOException"); 1579 assertThat(requestHandle.responseBodyError.getMessage()).contains("my error"); 1580 assertThat(requestHandle.completedSuccessfully).isFalse(); 1581 } 1582 1583 /** 1584 * If something fails when writing response body data to JNI, then we should *not* call any more 1585 * JNI callbacks again, since the native layer will already have handled the error. 1586 */ 1587 @Test testWriteResponseBodyToNativeFailureShouldNotCallJNICallback()1588 public void testWriteResponseBodyToNativeFailureShouldNotCallJNICallback() throws Exception { 1589 TestHttpRequestHandleImpl requestHandle = 1590 (TestHttpRequestHandleImpl) 1591 httpClient.enqueueRequest( 1592 JniHttpRequest.newBuilder() 1593 .setUri("https://foo.com") 1594 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1595 .setHasBody(false) 1596 .build() 1597 .toByteArray()); 1598 1599 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1600 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1601 1602 int expectedResponseCode = 300; 1603 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 1604 when(mockConnection.getHeaderFields()) 1605 .thenReturn(ImmutableMap.of("Response-Header1", ImmutableList.of("Bar"))); 1606 1607 // Make the response body contain some data. 1608 when(mockConnection.getInputStream()) 1609 .thenReturn(new ByteArrayInputStream("test_response".getBytes(UTF_8))); 1610 when(mockConnection.getContentLength()).thenReturn(-1); 1611 // But make the onResponseBody JNI method fail when it receives the data. 1612 requestHandle.onResponseBodyResult = false; 1613 1614 // Run the request. 1615 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1616 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1617 .isEqualTo(Code.OK_VALUE); 1618 1619 requestHandle.close(); 1620 1621 // Despite having hit an IOException during the response body download, we should still first 1622 // have passed the response headers to native. 1623 assertThat(requestHandle.responseProto) 1624 .isEqualTo( 1625 JniHttpResponse.newBuilder() 1626 .setCode(300) 1627 .addHeaders( 1628 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 1629 .build()); 1630 // No callbacks should have been invoked after we received the response headers. 1631 assertThat(requestHandle.responseError).isNull(); 1632 assertThat(requestHandle.responseBodyError).isNull(); 1633 assertThat(requestHandle.completedSuccessfully).isFalse(); 1634 } 1635 1636 @Test testCancellationDuringReceiveResponseBodyShouldResultInError()1637 public void testCancellationDuringReceiveResponseBodyShouldResultInError() throws Exception { 1638 TestHttpRequestHandleImpl requestHandle = 1639 (TestHttpRequestHandleImpl) 1640 httpClient.enqueueRequest( 1641 JniHttpRequest.newBuilder() 1642 .setUri("https://foo.com") 1643 .setMethod(JniHttpMethod.HTTP_METHOD_GET) 1644 .setHasBody(false) 1645 .build() 1646 .toByteArray()); 1647 1648 HttpURLConnection mockConnection = mock(HttpURLConnection.class); 1649 when(urlConnectionFactory.createUrlConnection(any())).thenReturn(mockConnection); 1650 1651 int expectedResponseCode = 300; 1652 when(mockConnection.getResponseCode()).thenReturn(expectedResponseCode); 1653 when(mockConnection.getResponseMessage()).thenReturn("Multiple Choices"); 1654 when(mockConnection.getHeaderFields()) 1655 .thenReturn( 1656 ImmutableMap.of( 1657 "Response-Header1", ImmutableList.of("Bar"), 1658 // The Content-Length header should be ignored, and should *not* be used to estimate 1659 // the 'received bytes', since the request will not complete successfully. 1660 "Content-Length", ImmutableList.of("9999"))); 1661 1662 // Make the response body contain some data. But when the data gets read, the request gets 1663 // cancelled. 1664 String fakeResponseBody = "test_response"; 1665 when(mockConnection.getInputStream()) 1666 .thenAnswer( 1667 invocation -> { 1668 requestHandle.close(); 1669 return new ByteArrayInputStream(fakeResponseBody.getBytes(UTF_8)); 1670 }); 1671 when(mockConnection.getContentLength()).thenReturn(-1); 1672 1673 // Run the request. 1674 byte[] result = httpClient.performRequests(new Object[] {requestHandle}); 1675 assertThat(Status.parseFrom(result, ExtensionRegistryLite.getEmptyRegistry()).getCode()) 1676 .isEqualTo(Code.OK_VALUE); 1677 1678 requestHandle.close(); 1679 1680 // Despite having hit a cancellation during the response body download, we should still first 1681 // have passed the response headers to native. 1682 assertThat(requestHandle.responseProto) 1683 .isEqualTo( 1684 JniHttpResponse.newBuilder() 1685 .setCode(300) 1686 .addHeaders( 1687 JniHttpHeader.newBuilder().setName("Response-Header1").setValue("Bar").build()) 1688 .build()); 1689 assertThat(requestHandle.responseError).isNull(); 1690 // The response body should not have been read to completion, since the request got cancelled 1691 // in the middle of the read. 1692 assertThat(requestHandle.responseBody.toString(UTF_8.name())).isNotEqualTo(fakeResponseBody); 1693 assertThat(requestHandle.responseBodyError).isNotNull(); 1694 assertThat(requestHandle.responseBodyError.getCode()).isEqualTo(Code.CANCELLED_VALUE); 1695 assertThat(requestHandle.completedSuccessfully).isFalse(); 1696 1697 // Verify the network stats are accurate (they should count the request headers, URL, request 1698 // method, request body, response headers and the *content length* rather than the observed 1699 // response body). 1700 assertThat( 1701 JniHttpSentReceivedBytes.parseFrom( 1702 requestHandle.getTotalSentReceivedBytes(), 1703 ExtensionRegistryLite.getEmptyRegistry())) 1704 .isEqualTo( 1705 JniHttpSentReceivedBytes.newBuilder() 1706 .setSentBytes("GET https://foo.com HTTP/1.1\r\n\r\n".length()) 1707 // The Content-Length response header value should not be taken into account in the 1708 // estimated 'received bytes' stat, since the request did not succeed. Instead, 1709 // by the time the HttpRequestHandleImpl#close() method is called we will be in the 1710 // process of having read a single buffer's worth of response body data, and hence 1711 // that's the amount of response body data that should be accounted for. This 1712 // ensures that we try as best as possible to only count bytes we actually received 1713 // up until the point of cancellation. 1714 .setReceivedBytes( 1715 ("HTTP/1.1 300 Multiple Choices\r\n" 1716 + "Response-Header1: Bar\r\n" 1717 + "Content-Length: 9999\r\n" 1718 + "\r\n") 1719 .length() 1720 + DEFAULT_TEST_CHUNK_BUFFER_SIZE) 1721 .build()); 1722 } 1723 } 1724