From cd2defbc42c157c61eff72ac154b806320484d9d Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 5 Nov 2026 21:50:54 +0000 Subject: [PATCH] fix: replace external nghttp2 ITs and cancel timed-out writes. Six HTTP/2 transport integration tests currently send live requests to https://nghttp2.org/httpbin. When that external service returns 502 Bad Gateway or becomes unreachable, nightly and release verification builds fail even though the SDK HTTP transport is healthy. In addition, when an outgoing request hits a write timeout, the transport leaves the underlying async HTTP request running in the background and drops the timeout exception cause. Move those six external HTTPbin cases into deterministic local unit tests using an embedded local HTTP server and backpressured async streams, while keeping the remaining network transport checks in the integration suite. Also cancel the in-flight response future on write timeouts (matching read timeouts) and shut down test transport instances cleanly in finally blocks. --- .../firebase/internal/ApacheHttp2Request.java | 3 +- .../internal/ApacheHttp2TransportIT.java | 189 ++------ .../internal/ApacheHttp2TransportTest.java | 426 +++++++++++++++++- 3 files changed, 467 insertions(+), 151 deletions(-) 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); } }