• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2022 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 package com.google.fcp.client.http;
15 
16 import static com.google.common.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