diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java index b06692ee8..ddcc28a9c 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -121,7 +121,8 @@ public void cancelled() { writeFuture.get(writeTimeout, TimeUnit.MILLISECONDS); } } catch (TimeoutException e) { - throw new IOException("Write Timeout", e.getCause()); + responseFuture.cancel(true); + throw new IOException("Write Timeout", e); } catch (Exception e) { throw new IOException("Exception in write", e.getCause()); } diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 7106ce5c5..8b17bbc5a 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -28,7 +28,6 @@ import com.google.api.client.http.HttpTransport; import com.google.api.client.http.LowLevelHttpResponse; import com.google.api.client.json.JsonFactory; -import com.google.api.client.util.GenericData; import com.google.auth.oauth2.GoogleCredentials; import com.google.common.collect.ImmutableMap; import com.google.firebase.ErrorCode; @@ -49,7 +48,6 @@ import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpRequestInterceptor; import org.apache.hc.core5.http.protocol.HttpContext; - import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -58,15 +56,9 @@ public class ApacheHttp2TransportIT { private static FirebaseApp app; private static final GoogleCredentials MOCK_CREDENTIALS = new MockGoogleCredentials("test_token"); - private static final ImmutableMap payload = + private static final ImmutableMap payload = ImmutableMap.of("foo", "bar"); - // Sets a 5 second delay before server response to simulate a slow network that - // results in a read timeout. - private static final String DELAY_URL = "https://nghttp2.org/httpbin/delay/5"; - private static final String GET_URL = "https://nghttp2.org/httpbin/get"; - private static final String POST_URL = "https://nghttp2.org/httpbin/post"; - private static ServerSocket serverSocket; private static Socket fillerSocket; private static int port; @@ -104,29 +96,13 @@ public void cleanup() { System.clearProperty("https.proxyPort"); } - @Test(timeout = 10_000L) - public void testUnauthorizedGetRequest() throws FirebaseException { - ErrorHandlingHttpClient httpClient = getHttpClient(false); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); - IncomingHttpResponse response = httpClient.send(request); - assertEquals(200, response.getStatusCode()); - } - - @Test(timeout = 10_000L) - public void testUnauthorizedPostRequest() throws FirebaseException { - ErrorHandlingHttpClient httpClient = getHttpClient(false); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(POST_URL, payload); - GenericData body = httpClient.sendAndParse(request, GenericData.class); - assertEquals("{\"foo\":\"bar\"}", body.get("data")); - } - @Test(timeout = 10_000L) public void testConnectTimeoutAuthorizedGet() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .setConnectTimeout(100) .build(), "test-app"); - ErrorHandlingHttpClient httpClient = getHttpClient(true, app); + ErrorHandlingHttpClient httpClient = getHttpClient(app); HttpRequestInfo request = HttpRequestInfo.buildGetRequest("https://localhost:" + port); try { @@ -145,8 +121,9 @@ public void testConnectTimeoutAuthorizedPost() throws FirebaseException { .setCredentials(MOCK_CREDENTIALS) .setConnectTimeout(100) .build(), "test-app"); - ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest("https://localhost:" + port, payload); + ErrorHandlingHttpClient httpClient = getHttpClient(app); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest( + "https://localhost:" + port, payload); try { httpClient.send(request); @@ -158,102 +135,25 @@ public void testConnectTimeoutAuthorizedPost() throws FirebaseException { } } - @Test(timeout = 10_000L) - public void testReadTimeoutAuthorizedGet() throws FirebaseException { - app = FirebaseApp.initializeApp(FirebaseOptions.builder() - .setCredentials(MOCK_CREDENTIALS) - .setReadTimeout(100) - .build(), "test-app"); - ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(DELAY_URL); - - try { - httpClient.send(request); - fail("No exception thrown for HTTP error response"); - } catch (FirebaseException e) { - assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - assertEquals("IO error: Stream exception in request", e.getMessage()); - assertNull(e.getHttpResponse()); - } - } - - @Test(timeout = 10_000L) - public void testReadTimeoutAuthorizedPost() throws FirebaseException { - app = FirebaseApp.initializeApp(FirebaseOptions.builder() - .setCredentials(MOCK_CREDENTIALS) - .setReadTimeout(100) - .build(), "test-app"); - ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(DELAY_URL, payload); - - try { - httpClient.send(request); - fail("No exception thrown for HTTP error response"); - } catch (FirebaseException e) { - assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - assertEquals("IO error: Stream exception in request", e.getMessage()); - assertNull(e.getHttpResponse()); - } - } - - @Test(timeout = 10_000L) - public void testWriteTimeoutAuthorizedGet() throws FirebaseException { - // Use a fresh transport so that writeTimeout triggers while waiting for the transport to - // be ready to receive data. - app = FirebaseApp.initializeApp(FirebaseOptions.builder() - .setCredentials(MOCK_CREDENTIALS) - .setWriteTimeout(100) - .setHttpTransport(new ApacheHttp2Transport()) - .build(), "test-app"); - ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); - - try { - httpClient.send(request); - fail("No exception thrown for HTTP error response"); - } catch (FirebaseException e) { - assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - assertEquals("IO error: Write Timeout", e.getMessage()); - assertNull(e.getHttpResponse()); - } - } - - @Test(timeout = 10_000L) - public void testWriteTimeoutAuthorizedPost() throws FirebaseException { - // Use a fresh transport so that writeTimeout triggers while waiting for the transport to - // be ready to receive data. - app = FirebaseApp.initializeApp(FirebaseOptions.builder() - .setCredentials(MOCK_CREDENTIALS) - .setWriteTimeout(100) - .setHttpTransport(new ApacheHttp2Transport()) - .build(), "test-app"); - ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(POST_URL, payload); - - try { - httpClient.send(request); - fail("No exception thrown for HTTP error response"); - } catch (FirebaseException e) { - assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - assertEquals("IO error: Write Timeout", e.getMessage()); - assertNull(e.getHttpResponse()); - } - } - @Test(timeout = 10_000L) public void testRequestShouldNotFollowRedirects() throws IOException { ApacheHttp2Transport transport = new ApacheHttp2Transport(); - ApacheHttp2Request request = transport.buildRequest("GET", - "https://google.com"); - LowLevelHttpResponse response = request.execute(); - - assertEquals(301, response.getStatusCode()); - assert (response instanceof ApacheHttp2Response); - assertEquals("https://www.google.com/", ((ApacheHttp2Response) response).getHeaderValue("location")); + try { + ApacheHttp2Request request = transport.buildRequest("GET", "https://google.com"); + LowLevelHttpResponse response = request.execute(); + + assertEquals(301, response.getStatusCode()); + assertTrue(response instanceof ApacheHttp2Response); + assertEquals( + "https://www.google.com/", + ((ApacheHttp2Response) response).getHeaderValue("location")); + } finally { + transport.shutdown(); + } } @Test(timeout = 10_000L) - public void testRequestCanSetHeaders() { + public void testRequestCanSetHeaders() throws IOException { final AtomicBoolean interceptorCalled = new AtomicBoolean(false); CloseableHttpAsyncClient client = HttpAsyncClients.custom() .addRequestInterceptorFirst( @@ -272,29 +172,37 @@ public void process( .build(); ApacheHttp2Transport transport = new ApacheHttp2Transport(client); - ApacheHttp2Request request = transport.buildRequest("GET", "http://www.google.com"); - request.addHeader("foo", "bar"); try { - request.execute(); - fail("should not actually make the request"); - } catch (IOException exception) { - assertEquals("Unknown exception in request", exception.getMessage()); + ApacheHttp2Request request = transport.buildRequest("GET", "http://www.google.com"); + request.addHeader("foo", "bar"); + try { + request.execute(); + fail("should not actually make the request"); + } catch (IOException exception) { + assertEquals("Unknown exception in request", exception.getMessage()); + } + assertTrue("Expected to have called our test interceptor", interceptorCalled.get()); + } finally { + transport.shutdown(); } - assertTrue("Expected to have called our test interceptor", interceptorCalled.get()); } @Test(timeout = 10_000L) - public void testVerifyProxyIsRespected() { - try { - System.setProperty("https.proxyHost", "localhost"); - System.setProperty("https.proxyPort", "8080"); + public void testVerifyProxyIsRespected() throws IOException { + System.setProperty("https.proxyHost", "localhost"); + System.setProperty("https.proxyPort", "8080"); - HttpTransport transport = new ApacheHttp2Transport(); - transport.createRequestFactory().buildGetRequest(new GenericUrl(GET_URL)).execute(); + HttpTransport transport = new ApacheHttp2Transport(); + try { + transport.createRequestFactory() + .buildGetRequest(new GenericUrl("https://www.google.com")) + .execute(); fail("No exception thrown for HTTP error response"); } catch (IOException e) { assertEquals("Connection exception in request", e.getMessage()); assertTrue(e.getCause().getMessage().contains("localhost:8080")); + } finally { + transport.shutdown(); } } @@ -303,7 +211,7 @@ public void testVerifyDefaultTransportReused() { FirebaseOptions o1 = FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .build(); - + FirebaseOptions o2 = FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .build(); @@ -313,26 +221,13 @@ public void testVerifyDefaultTransportReused() { assertEquals(t1, t2); } - private static ErrorHandlingHttpClient getHttpClient(boolean authorized, - FirebaseApp app) { - HttpRequestFactory requestFactory; - if (authorized) { - requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); - } else { - requestFactory = ApiClientUtils.newUnauthorizedRequestFactory(app); - } + private static ErrorHandlingHttpClient getHttpClient(FirebaseApp app) { + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); TestHttpErrorHandler errorHandler = new TestHttpErrorHandler(); return new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler); } - private static ErrorHandlingHttpClient getHttpClient(boolean authorized) { - app = FirebaseApp.initializeApp(FirebaseOptions.builder() - .setCredentials(MOCK_CREDENTIALS) - .build(), "test-app"); - return getHttpClient(authorized, app); - } - private static class TestHttpErrorHandler implements HttpErrorHandler { @Override public FirebaseException handleIOException(IOException e) { diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java index e6d80847a..7631447c0 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java @@ -19,25 +19,44 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.api.client.http.ByteArrayContent; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpContent; import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.InputStreamContent; import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.json.JsonFactory; import com.google.api.client.util.ByteArrayStreamingContent; - +import com.google.api.client.util.GenericData; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.ByteStreams; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; - +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.apache.hc.client5.http.async.HttpAsyncClient; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; @@ -48,7 +67,7 @@ import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; @@ -65,13 +84,23 @@ import org.apache.hc.core5.http.nio.AsyncPushConsumer; import org.apache.hc.core5.http.nio.AsyncRequestProducer; import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.DataStreamChannel; import org.apache.hc.core5.http.nio.HandlerFactory; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpProcessorBuilder; +import org.junit.After; import org.junit.Assert; import org.junit.Test; public class ApacheHttp2TransportTest { + private static final ImmutableMap TEST_PAYLOAD = + ImmutableMap.of("foo", "bar"); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + @Test public void testContentLengthSet() throws Exception { SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(HttpMethods.POST) @@ -451,7 +480,398 @@ public void handle( Assert.fail("Expected IOException on read timeout"); } catch (IOException e) { assertEquals("Stream exception in request", e.getMessage()); + } finally { + transport.shutdown(); + } + } + } + + @Test(timeout = 10_000L) + public void testUnauthorizedGetRequest() throws Exception { + final AtomicReference receivedMethod = new AtomicReference<>(); + final AtomicReference receivedAuth = new AtomicReference<>(); + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) { + receivedMethod.set(request.getMethod()); + Header authHeader = request.getFirstHeader(HttpHeaders.AUTHORIZATION); + if (authHeader != null) { + receivedAuth.set(authHeader.getValue()); + } + byte[] responseData = "{\"result\":\"ok\"}".getBytes(StandardCharsets.UTF_8); + response.setCode(HttpStatus.SC_OK); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length)); + response.setEntity(new ByteArrayEntity(responseData, ContentType.APPLICATION_JSON)); } + }; + + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + try { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test_token")) + .setHttpTransport(transport) + .build()); + String testUrl = "http://localhost:" + server.getPort() + "/get"; + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(testUrl); + IncomingHttpResponse response = createHttpClient(false, app).send(request); + assertEquals(200, response.getStatusCode()); + assertEquals("{\"result\":\"ok\"}", response.getContent()); + assertEquals("GET", receivedMethod.get()); + assertNull(receivedAuth.get()); + } finally { + transport.shutdown(); + } + } + } + + @Test(timeout = 10_000L) + public void testUnauthorizedPostRequest() throws Exception { + final AtomicReference receivedMethod = new AtomicReference<>(); + final AtomicReference receivedAuth = new AtomicReference<>(); + final AtomicReference receivedContentType = new AtomicReference<>(); + final AtomicReference receivedBody = new AtomicReference<>(); + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws IOException { + receivedMethod.set(request.getMethod()); + Header authHeader = request.getFirstHeader(HttpHeaders.AUTHORIZATION); + if (authHeader != null) { + receivedAuth.set(authHeader.getValue()); + } + if (request.getEntity() != null) { + receivedContentType.set(request.getEntity().getContentType()); + byte[] requestBytes = ByteStreams.toByteArray(request.getEntity().getContent()); + receivedBody.set(new String(requestBytes, StandardCharsets.UTF_8)); + } + byte[] responseData = "{\"data\":\"{\\\"foo\\\":\\\"bar\\\"}\"}" + .getBytes(StandardCharsets.UTF_8); + response.setCode(HttpStatus.SC_OK); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length)); + response.setEntity(new ByteArrayEntity(responseData, ContentType.APPLICATION_JSON)); + } + }; + + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + try { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test_token")) + .setHttpTransport(transport) + .build()); + String testUrl = "http://localhost:" + server.getPort() + "/post"; + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(testUrl, TEST_PAYLOAD); + GenericData body = createHttpClient(false, app).sendAndParse(request, GenericData.class); + assertEquals("{\"foo\":\"bar\"}", body.get("data")); + assertEquals("POST", receivedMethod.get()); + assertNull(receivedAuth.get()); + assertNotNull(receivedContentType.get()); + assertTrue(receivedContentType.get().startsWith("application/json")); + assertEquals("{\"foo\":\"bar\"}", receivedBody.get()); + } finally { + transport.shutdown(); + } + } + } + + @Test(timeout = 10_000L) + public void testAuthorizedPostRequest() throws Exception { + final AtomicReference receivedMethod = new AtomicReference<>(); + final AtomicReference receivedAuth = new AtomicReference<>(); + final AtomicReference receivedContentType = new AtomicReference<>(); + final AtomicReference receivedBody = new AtomicReference<>(); + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws IOException { + receivedMethod.set(request.getMethod()); + Header authHeader = request.getFirstHeader(HttpHeaders.AUTHORIZATION); + if (authHeader != null) { + receivedAuth.set(authHeader.getValue()); + } + if (request.getEntity() != null) { + receivedContentType.set(request.getEntity().getContentType()); + byte[] requestBytes = ByteStreams.toByteArray(request.getEntity().getContent()); + receivedBody.set(new String(requestBytes, StandardCharsets.UTF_8)); + } + byte[] responseData = "{\"result\":\"ok\"}".getBytes(StandardCharsets.UTF_8); + response.setCode(HttpStatus.SC_OK); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length)); + response.setEntity(new ByteArrayEntity(responseData, ContentType.APPLICATION_JSON)); + } + }; + + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + try { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test_token")) + .setHttpTransport(transport) + .build()); + String testUrl = "http://localhost:" + server.getPort() + "/post"; + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(testUrl, TEST_PAYLOAD); + GenericData body = createHttpClient(true, app).sendAndParse(request, GenericData.class); + assertEquals("ok", body.get("result")); + assertEquals("POST", receivedMethod.get()); + assertEquals("Bearer test_token", receivedAuth.get()); + assertNotNull(receivedContentType.get()); + assertTrue(receivedContentType.get().startsWith("application/json")); + assertEquals("{\"foo\":\"bar\"}", receivedBody.get()); + } finally { + transport.shutdown(); + } + } + } + + @Test(timeout = 10_000L) + public void testReadTimeoutAuthorizedGet() throws Exception { + final CountDownLatch requestReceived = new CountDownLatch(1); + final CountDownLatch releaseResponse = new CountDownLatch(1); + final AtomicReference receivedAuth = new AtomicReference<>(); + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) { + Header authHeader = request.getFirstHeader(HttpHeaders.AUTHORIZATION); + if (authHeader != null) { + receivedAuth.set(authHeader.getValue()); + } + requestReceived.countDown(); + try { + releaseResponse.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + response.setCode(HttpStatus.SC_OK); + } + }; + + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + try { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test_token")) + .setReadTimeout(100) + .setHttpTransport(transport) + .build()); + String testUrl = "http://localhost:" + server.getPort() + "/delay"; + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(testUrl); + + try { + createHttpClient(true, app).send(request); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Stream exception in request", e.getMessage()); + assertNull(e.getHttpResponse()); + } + assertTrue(requestReceived.await(1, TimeUnit.SECONDS)); + assertEquals("Bearer test_token", receivedAuth.get()); + } finally { + releaseResponse.countDown(); + transport.shutdown(); + } + } + } + + @Test(timeout = 10_000L) + public void testReadTimeoutAuthorizedPost() throws Exception { + final CountDownLatch requestReceived = new CountDownLatch(1); + final CountDownLatch releaseResponse = new CountDownLatch(1); + final AtomicReference receivedAuth = new AtomicReference<>(); + final AtomicReference receivedBody = new AtomicReference<>(); + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws IOException { + Header authHeader = request.getFirstHeader(HttpHeaders.AUTHORIZATION); + if (authHeader != null) { + receivedAuth.set(authHeader.getValue()); + } + if (request.getEntity() != null) { + byte[] requestBytes = ByteStreams.toByteArray(request.getEntity().getContent()); + receivedBody.set(new String(requestBytes, StandardCharsets.UTF_8)); + } + requestReceived.countDown(); + try { + releaseResponse.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + response.setCode(HttpStatus.SC_OK); + } + }; + + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + try { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test_token")) + .setReadTimeout(100) + .setHttpTransport(transport) + .build()); + String testUrl = "http://localhost:" + server.getPort() + "/delay"; + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(testUrl, TEST_PAYLOAD); + + try { + createHttpClient(true, app).send(request); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Stream exception in request", e.getMessage()); + assertNull(e.getHttpResponse()); + } + assertTrue(requestReceived.await(1, TimeUnit.SECONDS)); + assertEquals("Bearer test_token", receivedAuth.get()); + assertEquals("{\"foo\":\"bar\"}", receivedBody.get()); + } finally { + releaseResponse.countDown(); + transport.shutdown(); + } + } + } + + @Test(timeout = 10_000L) + public void testWriteTimeoutAuthorizedGet() throws IOException { + final AtomicReference> responseFutureRef = new AtomicReference<>(); + CloseableHttpAsyncClient stalledClient = new MockApacheHttp2AsyncClient() { + @Override + protected Future doExecute( + HttpHost target, + AsyncRequestProducer requestProducer, + AsyncResponseConsumer responseConsumer, + HandlerFactory pushHandlerFactory, + HttpContext context, + FutureCallback callback) { + CompletableFuture future = new CompletableFuture<>(); + responseFutureRef.set(future); + return future; + } + }; + + HttpTransport transport = new ApacheHttp2Transport(stalledClient); + try { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test_token")) + .setWriteTimeout(100) + .setHttpTransport(transport) + .build()); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest("https://example.com/get"); + + try { + createHttpClient(true, app).send(request); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Write Timeout", e.getMessage()); + assertNull(e.getHttpResponse()); + assertTrue(e.getCause() instanceof IOException); + assertTrue(e.getCause().getCause() instanceof TimeoutException); + } + assertNotNull(responseFutureRef.get()); + assertTrue(responseFutureRef.get().isCancelled()); + } finally { + transport.shutdown(); + } + } + + @Test(timeout = 10_000L) + public void testWriteTimeoutAuthorizedPost() throws IOException { + final AtomicInteger bytesOffered = new AtomicInteger(); + final AtomicReference> responseFutureRef = new AtomicReference<>(); + CloseableHttpAsyncClient backpressuredClient = new MockApacheHttp2AsyncClient() { + @Override + protected Future doExecute( + HttpHost target, + AsyncRequestProducer requestProducer, + AsyncResponseConsumer responseConsumer, + HandlerFactory pushHandlerFactory, + HttpContext context, + FutureCallback callback) { + try { + requestProducer.produce(new DataStreamChannel() { + @Override + public void requestOutput() {} + + @Override + public int write(ByteBuffer src) { + bytesOffered.set(src.remaining()); + return 0; + } + + @Override + public void endStream() {} + + @Override + public void endStream(List trailers) {} + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + CompletableFuture future = new CompletableFuture<>(); + responseFutureRef.set(future); + return future; + } + }; + + HttpTransport transport = new ApacheHttp2Transport(backpressuredClient); + try { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test_token")) + .setWriteTimeout(100) + .setHttpTransport(transport) + .build()); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest( + "https://example.com/post", TEST_PAYLOAD); + + try { + createHttpClient(true, app).send(request); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Write Timeout", e.getMessage()); + assertNull(e.getHttpResponse()); + assertTrue(e.getCause() instanceof IOException); + assertTrue(e.getCause().getCause() instanceof TimeoutException); + } + assertEquals("{\"foo\":\"bar\"}".length(), bytesOffered.get()); + assertNotNull(responseFutureRef.get()); + assertTrue(responseFutureRef.get().isCancelled()); + } finally { + transport.shutdown(); + } + } + + private static ErrorHandlingHttpClient createHttpClient( + boolean authorized, FirebaseApp app) { + HttpRequestFactory requestFactory = authorized + ? ApiClientUtils.newAuthorizedRequestFactory(app) + : ApiClientUtils.newUnauthorizedRequestFactory(app); + JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); + return new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, new TestHttpErrorHandler()); + } + + private static class TestHttpErrorHandler implements HttpErrorHandler { + @Override + public FirebaseException handleIOException(IOException e) { + return new FirebaseException(ErrorCode.UNKNOWN, "IO error: " + e.getMessage(), e); + } + + @Override + public FirebaseException handleHttpResponseException( + HttpResponseException e, IncomingHttpResponse response) { + return new FirebaseException( + ErrorCode.INTERNAL, "Example error message: " + e.getContent(), e, response); + } + + @Override + public FirebaseException handleParseException(IOException e, IncomingHttpResponse response) { + return new FirebaseException(ErrorCode.UNKNOWN, "Parse error", e, response); } }