From e0f3e212a1df5f874d6396a60877960ec321e5e3 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Wed, 14 Aug 2024 15:44:06 -0400 Subject: [PATCH 01/21] Added HTTP/2 enabled transport and made it default. --- pom.xml | 5 + .../ApacheHttp2AsyncEntityProducer.java | 113 ++++++++++++++++++ .../firebase/internal/ApacheHttp2Request.java | 108 +++++++++++++++++ .../internal/ApacheHttp2Response.java | 84 +++++++++++++ .../internal/ApacheHttp2Transport.java | 75 ++++++++++++ .../firebase/internal/ApiClientUtils.java | 2 +- 6 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2Request.java create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2Response.java create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java diff --git a/pom.xml b/pom.xml index 128632f95..2e7a09a3b 100644 --- a/pom.xml +++ b/pom.xml @@ -455,6 +455,11 @@ netty-transport ${netty.version} + + org.apache.httpcomponents.client5 + httpclient5 + 5.3.1 + diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java new file mode 100644 index 000000000..aa4335df1 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java @@ -0,0 +1,113 @@ +package com.google.firebase.internal; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.DataStreamChannel; + +import com.google.api.client.util.StreamingContent; + +@SuppressWarnings("deprecation") +public class ApacheHttp2AsyncEntityProducer implements AsyncEntityProducer { + private final ByteBuffer bytebuf; + private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private final ContentType contentType; + private final long contentLength; + private final String contentEncoding; + private final CompletableFuture writeFuture; + private final AtomicReference exception; + + public ApacheHttp2AsyncEntityProducer(final StreamingContent content, final ContentType contentType, String contentEncoding, long contentLength, CompletableFuture writeFuture) { + this.writeFuture = writeFuture; + + try { + content.writeTo(baos); + } catch (IOException e) { + writeFuture.completeExceptionally(e); + } + this.bytebuf = ByteBuffer.wrap(baos.toByteArray()); + this.contentType = contentType; + this.contentLength = contentLength; + this.contentEncoding = contentEncoding; + this.exception = new AtomicReference<>(); + } + + public ApacheHttp2AsyncEntityProducer(ApacheHttp2Request request, CompletableFuture writeFuture) { + this( + request.getStreamingContent(), + ContentType.parse(request.getContentType()), + request.getContentEncoding(), + request.getContentLength(), + writeFuture + ); + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public String getContentType() { + return contentType != null ? contentType.toString() : null; + } + + @Override + public long getContentLength() { + return contentLength; + } + + @Override + public int available() { + return Integer.MAX_VALUE; + } + + @Override + public String getContentEncoding() { + return contentEncoding; + } + + @Override + public boolean isChunked() { + return false; + } + + @Override + public Set getTrailerNames() { + return null; + } + + @Override + public void produce(DataStreamChannel channel) throws IOException { + if (bytebuf.hasRemaining()) { + channel.write(bytebuf); + } + if (!bytebuf.hasRemaining()) { + channel.endStream(); + writeFuture.complete(null); + } + } + + @Override + public void failed(Exception cause) { + if (exception.compareAndSet(null, cause)) { + releaseResources(); + writeFuture.completeExceptionally(cause); + } + } + + public final Exception getException() { + return exception.get(); + } + + @Override + public void releaseResources() { + bytebuf.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java new file mode 100644 index 000000000..dc557600a --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -0,0 +1,108 @@ +package com.google.firebase.internal; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.util.Timeout; + +@SuppressWarnings("deprecation") +final class ApacheHttp2Request extends LowLevelHttpRequest { + private final CloseableHttpAsyncClient httpAsyncClient; + private final SimpleRequestBuilder requestBuilder; + private SimpleHttpRequest request; + private final RequestConfig.Builder requestConfig; + private int writeTimeout; + + ApacheHttp2Request( + CloseableHttpAsyncClient httpAsyncClient, SimpleRequestBuilder requestBuilder) { + this.httpAsyncClient = httpAsyncClient; + this.requestBuilder = requestBuilder; + this.writeTimeout = 0; + + this.requestConfig = RequestConfig.custom() + .setRedirectsEnabled(false); + } + + @Override + public void addHeader(String name, String value) { + requestBuilder.addHeader(name, value); + } + + @Override + public void setTimeout(int connectionTimeout, int readTimeout) throws IOException { + requestConfig + .setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)) + .setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); + } + + @Override + public void setWriteTimeout(int writeTimeout) throws IOException { + this.writeTimeout = writeTimeout; + } + + @Override + public LowLevelHttpResponse execute() throws IOException { + // Set request configs + requestBuilder.setRequestConfig(requestConfig.build()); + + // Build request + request = requestBuilder.build(); + + // Make Producer + CompletableFuture writeFuture = new CompletableFuture<>(); + ApacheHttp2AsyncEntityProducer entityProducer = new ApacheHttp2AsyncEntityProducer(this, writeFuture); + + // Execute + final CompletableFuture responseFuture = new CompletableFuture<>(); + try { + httpAsyncClient.execute( + new BasicRequestProducer(request, entityProducer), + SimpleResponseConsumer.create(), + new FutureCallback() { + @Override + public void completed(final SimpleHttpResponse response) { + responseFuture.complete(response); + } + + @Override + public void failed(final Exception exception) { + responseFuture.completeExceptionally(exception); + } + + @Override + public void cancelled() { + responseFuture.cancel(false); + } + }); + + if (writeTimeout != 0) { + writeFuture.get(writeTimeout, TimeUnit.MILLISECONDS); + } else { + // writeFuture.get(); + } + + final SimpleHttpResponse response = responseFuture.get(); + return new ApacheHttp2Response(request, response); + } catch (InterruptedException e) { + throw new IOException("Request Interrupted", e); + } catch (ExecutionException e) { + throw new IOException("Exception in request", e); + } catch (TimeoutException e) { + throw new IOException("Timed out", e); + } + } +} diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java new file mode 100644 index 000000000..fd20015ac --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java @@ -0,0 +1,84 @@ +package com.google.firebase.internal; + +import com.google.api.client.http.LowLevelHttpResponse; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.core5.http.Header; + +public class ApacheHttp2Response extends LowLevelHttpResponse { + + private final SimpleHttpResponse response; + private final Header[] allHeaders; + + ApacheHttp2Response(SimpleHttpRequest request, SimpleHttpResponse response) { + this.response = response; + allHeaders = response.getHeaders(); + } + + @Override + public int getStatusCode() { + return response.getCode(); + } + + @Override + public InputStream getContent() throws IOException { + return new ByteArrayInputStream(response.getBodyBytes()); + } + + @Override + public String getContentEncoding() { + Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); + if (contentEncodingHeader == null) { + return null; + } + return contentEncodingHeader.getValue(); + } + + @Override + public long getContentLength() { + return response.getBodyText().length(); + } + + @Override + public String getContentType() { + return response.getContentType().toString(); + } + + @Override + public String getReasonPhrase() { + return response.getReasonPhrase(); + } + + @Override + public String getStatusLine() { + return response.toString(); + } + + public String getHeaderValue(String name) { + Header header = response.getLastHeader(name); + if (header == null) { + return null; + } + return header.getValue(); + } + + @Override + public String getHeaderValue(int index) { + return allHeaders[index].getValue(); + } + + @Override + public int getHeaderCount() { + return allHeaders.length; + } + + @Override + public String getHeaderName(int index) { + return allHeaders[index].getName(); + } +} diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java new file mode 100644 index 000000000..a94bb7ecd --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java @@ -0,0 +1,75 @@ +package com.google.firebase.internal; + +import com.google.api.client.http.HttpTransport; + +import java.io.IOException; +import java.net.ProxySelector; +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.async.HttpAsyncClient; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.config.TlsConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; +import org.apache.hc.core5.http.config.Http1Config; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.util.TimeValue; + +public final class ApacheHttp2Transport extends HttpTransport { + + public final CloseableHttpAsyncClient httpAsyncClient; + + public ApacheHttp2Transport() { + this(newDefaultHttpAsyncClient()); + } + + public ApacheHttp2Transport(CloseableHttpAsyncClient httpAsyncClient) { + this.httpAsyncClient = httpAsyncClient; + httpAsyncClient.start(); + } + + public static CloseableHttpAsyncClient newDefaultHttpAsyncClient() { + return defaultHttpAsyncClientBuilder().build(); + } + + public static HttpAsyncClientBuilder defaultHttpAsyncClientBuilder() { + PoolingAsyncClientConnectionManager connectionManager = + new PoolingAsyncClientConnectionManager(); + connectionManager.setMaxTotal(100); + connectionManager.setDefaultMaxPerRoute(100); + connectionManager.closeIdle(TimeValue.of(30, TimeUnit.SECONDS)); + connectionManager.setDefaultTlsConfig( + TlsConfig.custom().setVersionPolicy(HttpVersionPolicy.NEGOTIATE).build()); + + return HttpAsyncClientBuilder.create() + .setH2Config(H2Config.DEFAULT) + .setHttp1Config(Http1Config.DEFAULT) + .setConnectionManager(connectionManager) + .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault())) + .disableRedirectHandling() + .disableAutomaticRetries(); + } + + @Override + public boolean supportsMethod(String method) { + return true; + } + + @Override + protected ApacheHttp2Request buildRequest(String method, String url) { + SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(method).setUri(url); + return new ApacheHttp2Request(httpAsyncClient, requestBuilder); + } + + @Override + public void shutdown() throws IOException { + httpAsyncClient.close(); + } + + public HttpAsyncClient getHttpClient() { + return httpAsyncClient; + } +} diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java index e586bd11f..339726105 100644 --- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -88,6 +88,6 @@ public static JsonFactory getDefaultJsonFactory() { } public static HttpTransport getDefaultTransport() { - return Utils.getDefaultTransport(); + return new ApacheHttp2Transport(); } } From 93016ebc588e8ed9ea75a90e797024d3f44ce38b Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Wed, 14 Aug 2024 15:47:43 -0400 Subject: [PATCH 02/21] fix: Use internal default transport --- .../java/com/google/firebase/FirebaseOptionsTest.java | 4 ++-- .../messaging/FirebaseMessagingClientImplTest.java | 10 +++++----- .../firebase/messaging/InstanceIdClientImplTest.java | 2 +- .../FirebaseRemoteConfigClientImplTest.java | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java index 0a4e9e81a..05613e98a 100644 --- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java +++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java @@ -24,12 +24,12 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.cloud.firestore.FirestoreOptions; +import com.google.firebase.internal.ApacheHttp2Transport; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; import java.io.IOException; @@ -74,7 +74,7 @@ protected ThreadFactory getThreadFactory() { @Test public void createOptionsWithAllValuesSet() throws IOException { GsonFactory jsonFactory = new GsonFactory(); - NetHttpTransport httpTransport = new NetHttpTransport(); + ApacheHttp2Transport httpTransport = new ApacheHttp2Transport(); FirestoreOptions firestoreOptions = FirestoreOptions.newBuilder().build(); FirebaseOptions firebaseOptions = FirebaseOptions.builder() diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index 17848cc65..6eabd8e7c 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -358,7 +358,7 @@ public void initialize(HttpRequest httpRequest) { .setProjectId("test-project") .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) .setRequestFactory(transport.createRequestFactory(initializer)) - .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setChildRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()) .setResponseInterceptor(interceptor) .build(); @@ -558,7 +558,7 @@ private FirebaseMessagingClientImpl initMessagingClient( .setProjectId("test-project") .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) .setRequestFactory(transport.createRequestFactory()) - .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setChildRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()) .setResponseInterceptor(interceptor) .build(); } @@ -581,7 +581,7 @@ private FirebaseMessagingClientImpl initClientWithFaultyTransport() { .setProjectId("test-project") .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) .setRequestFactory(transport.createRequestFactory()) - .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setChildRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()) .build(); } @@ -665,8 +665,8 @@ private FirebaseMessagingClientImpl.Builder fullyPopulatedBuilder() { return FirebaseMessagingClientImpl.builder() .setProjectId("test-project") .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(Utils.getDefaultTransport().createRequestFactory()) - .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()); + .setRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()) + .setChildRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()); } private void checkExceptionFromHttpResponse( diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java index e7222ccb5..1a140680f 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -358,7 +358,7 @@ public void testRequestFactoryIsNull() { @Test(expected = NullPointerException.class) public void testJsonFactoryIsNull() { - new InstanceIdClientImpl(Utils.getDefaultTransport().createRequestFactory(), null); + new InstanceIdClientImpl(ApiClientUtils.getDefaultTransport().createRequestFactory(), null); } @Test diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index f2d5c8126..0a04809cf 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -1173,7 +1173,7 @@ private FirebaseRemoteConfigClientImpl.Builder fullyPopulatedBuilder() { return FirebaseRemoteConfigClientImpl.builder() .setProjectId("test-project") .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(Utils.getDefaultTransport().createRequestFactory()); + .setRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()); } private void checkGetRequestHeader(HttpRequest request) { From 1541ad03aa4c515b2a57e806fe5b54cd7c8b08fd Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Thu, 15 Aug 2024 16:05:37 -0400 Subject: [PATCH 03/21] Added test coverage for timeouts --- .../ApacheHttp2AsyncEntityProducer.java | 10 +- .../firebase/internal/ApacheHttp2Request.java | 4 +- .../internal/ApacheHttp2TransportIT.java | 203 ++++++++++++++++++ 3 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java index aa4335df1..26ce14130 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java @@ -26,10 +26,12 @@ public class ApacheHttp2AsyncEntityProducer implements AsyncEntityProducer { public ApacheHttp2AsyncEntityProducer(final StreamingContent content, final ContentType contentType, String contentEncoding, long contentLength, CompletableFuture writeFuture) { this.writeFuture = writeFuture; - try { - content.writeTo(baos); - } catch (IOException e) { - writeFuture.completeExceptionally(e); + if (content != null) { + try { + content.writeTo(baos); + } catch (IOException e) { + writeFuture.completeExceptionally(e); + } } this.bytebuf = ByteBuffer.wrap(baos.toByteArray()); this.contentType = contentType; diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java index dc557600a..03581ec3b 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -19,7 +19,6 @@ import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.util.Timeout; -@SuppressWarnings("deprecation") final class ApacheHttp2Request extends LowLevelHttpRequest { private final CloseableHttpAsyncClient httpAsyncClient; private final SimpleRequestBuilder requestBuilder; @@ -43,6 +42,7 @@ public void addHeader(String name, String value) { } @Override + @SuppressWarnings("deprecation") public void setTimeout(int connectionTimeout, int readTimeout) throws IOException { requestConfig .setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)) @@ -91,8 +91,6 @@ public void cancelled() { if (writeTimeout != 0) { writeFuture.get(writeTimeout, TimeUnit.MILLISECONDS); - } else { - // writeFuture.get(); } final SimpleHttpResponse response = responseFuture.get(); diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java new file mode 100644 index 000000000..aebb08834 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -0,0 +1,203 @@ +package com.google.firebase.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.io.IOException; + +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponseException; +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; +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; + +public class ApacheHttp2TransportIT { + private static final GoogleCredentials MOCK_CREDENTIALS = new MockGoogleCredentials("test_token"); + private static final ImmutableMap payload = ImmutableMap.of("foo", "bar"); + // Sets a 1 second delay before response + private static final String DELAY_URL = "https://nghttp2.org/httpbin/delay/1"; + private static final String POST_URL = "https://nghttp2.org/httpbin/post"; + + @BeforeClass + public static void setUpClass() { + } + + @After + public void cleanup() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + 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 + public void testConnectTimeoutAuthorizedGet() throws FirebaseException { + FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setConnectTimeout(1) + .build()); + ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + 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: Exception in request", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test + public void testConnectTimeoutAuthorizedPost() throws FirebaseException { + FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setConnectTimeout(1) + .build()); + ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + 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: Exception in request", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test + public void testReadTimeoutAuthorizedGet() throws FirebaseException { + FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setReadTimeout(1) + .build()); + ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + 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: Exception in request", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test + public void testReadTimeoutAuthorizedPost() throws FirebaseException { + FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setReadTimeout(1) + .build()); + ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + 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: Exception in request", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test + public void testWriteTimeoutAuthorizedGet() throws FirebaseException { + FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setWriteTimeout(1) + .build()); + ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + 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: Timed out", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + @Test + public void testWriteTimeoutAuthorizedPost() throws FirebaseException { + FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setWriteTimeout(1) + .build()); + ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + 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: Timed out", e.getMessage()); + assertNull(e.getHttpResponse()); + } + } + + private static ErrorHandlingHttpClient getHttpClient(boolean authorized, FirebaseApp app) { + HttpRequestFactory requestFactory; + if (authorized) { + requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + } else { + requestFactory = ApiClientUtils.newUnauthorizedRequestFactory(app); + } + JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); + TestHttpErrorHandler errorHandler = new TestHttpErrorHandler(); + return new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler); + } + + private static ErrorHandlingHttpClient getHttpClient(boolean authorized) { + return getHttpClient(authorized, FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .build(), "test-app")); + } + + + 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); + } + } +} From d78632c058fd68138ae2fdfc5270ca48091a4277 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Thu, 15 Aug 2024 16:29:03 -0400 Subject: [PATCH 04/21] fix: lint --- .../ApacheHttp2AsyncEntityProducer.java | 185 +++++++++--------- .../firebase/internal/ApacheHttp2Request.java | 3 +- .../internal/ApacheHttp2TransportIT.java | 18 +- 3 files changed, 105 insertions(+), 101 deletions(-) diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java index 26ce14130..546977239 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java @@ -1,5 +1,7 @@ package com.google.firebase.internal; +import com.google.api.client.util.StreamingContent; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; @@ -11,105 +13,104 @@ import org.apache.hc.core5.http.nio.AsyncEntityProducer; import org.apache.hc.core5.http.nio.DataStreamChannel; -import com.google.api.client.util.StreamingContent; - @SuppressWarnings("deprecation") public class ApacheHttp2AsyncEntityProducer implements AsyncEntityProducer { - private final ByteBuffer bytebuf; - private ByteArrayOutputStream baos = new ByteArrayOutputStream(); - private final ContentType contentType; - private final long contentLength; - private final String contentEncoding; - private final CompletableFuture writeFuture; - private final AtomicReference exception; - - public ApacheHttp2AsyncEntityProducer(final StreamingContent content, final ContentType contentType, String contentEncoding, long contentLength, CompletableFuture writeFuture) { - this.writeFuture = writeFuture; - - if (content != null) { - try { - content.writeTo(baos); - } catch (IOException e) { - writeFuture.completeExceptionally(e); - } - } - this.bytebuf = ByteBuffer.wrap(baos.toByteArray()); - this.contentType = contentType; - this.contentLength = contentLength; - this.contentEncoding = contentEncoding; - this.exception = new AtomicReference<>(); - } - - public ApacheHttp2AsyncEntityProducer(ApacheHttp2Request request, CompletableFuture writeFuture) { - this( - request.getStreamingContent(), - ContentType.parse(request.getContentType()), - request.getContentEncoding(), - request.getContentLength(), - writeFuture - ); - } - - @Override - public boolean isRepeatable() { - return false; - } - - @Override - public String getContentType() { - return contentType != null ? contentType.toString() : null; - } - - @Override - public long getContentLength() { - return contentLength; - } - - @Override - public int available() { - return Integer.MAX_VALUE; + private final ByteBuffer bytebuf; + private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private final ContentType contentType; + private final long contentLength; + private final String contentEncoding; + private final CompletableFuture writeFuture; + private final AtomicReference exception; + + public ApacheHttp2AsyncEntityProducer(StreamingContent content, ContentType contentType, + String contentEncoding, long contentLength, CompletableFuture writeFuture) { + this.writeFuture = writeFuture; + + if (content != null) { + try { + content.writeTo(baos); + } catch (IOException e) { + writeFuture.completeExceptionally(e); + } } - - @Override - public String getContentEncoding() { - return contentEncoding; + this.bytebuf = ByteBuffer.wrap(baos.toByteArray()); + this.contentType = contentType; + this.contentLength = contentLength; + this.contentEncoding = contentEncoding; + this.exception = new AtomicReference<>(); + } + + public ApacheHttp2AsyncEntityProducer(ApacheHttp2Request request, + CompletableFuture writeFuture) { + this( + request.getStreamingContent(), + ContentType.parse(request.getContentType()), + request.getContentEncoding(), + request.getContentLength(), + writeFuture); + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public String getContentType() { + return contentType != null ? contentType.toString() : null; + } + + @Override + public long getContentLength() { + return contentLength; + } + + @Override + public int available() { + return Integer.MAX_VALUE; + } + + @Override + public String getContentEncoding() { + return contentEncoding; + } + + @Override + public boolean isChunked() { + return false; + } + + @Override + public Set getTrailerNames() { + return null; + } + + @Override + public void produce(DataStreamChannel channel) throws IOException { + if (bytebuf.hasRemaining()) { + channel.write(bytebuf); } - - @Override - public boolean isChunked() { - return false; + if (!bytebuf.hasRemaining()) { + channel.endStream(); + writeFuture.complete(null); } + } - @Override - public Set getTrailerNames() { - return null; + @Override + public void failed(Exception cause) { + if (exception.compareAndSet(null, cause)) { + releaseResources(); + writeFuture.completeExceptionally(cause); } + } - @Override - public void produce(DataStreamChannel channel) throws IOException { - if (bytebuf.hasRemaining()) { - channel.write(bytebuf); - } - if (!bytebuf.hasRemaining()) { - channel.endStream(); - writeFuture.complete(null); - } - } + public final Exception getException() { + return exception.get(); + } - @Override - public void failed(Exception cause) { - if (exception.compareAndSet(null, cause)) { - releaseResources(); - writeFuture.completeExceptionally(cause); - } - } - - public final Exception getException() { - return exception.get(); - } - - @Override - public void releaseResources() { - bytebuf.clear(); - } + @Override + public void releaseResources() { + bytebuf.clear(); + } } \ No newline at end of file diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java index 03581ec3b..76cc59ad0 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -64,7 +64,8 @@ public LowLevelHttpResponse execute() throws IOException { // Make Producer CompletableFuture writeFuture = new CompletableFuture<>(); - ApacheHttp2AsyncEntityProducer entityProducer = new ApacheHttp2AsyncEntityProducer(this, writeFuture); + ApacheHttp2AsyncEntityProducer entityProducer = + new ApacheHttp2AsyncEntityProducer(this, writeFuture); // Execute final CompletableFuture responseFuture = new CompletableFuture<>(); diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index aebb08834..d46fd8e5d 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -4,12 +4,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; -import java.io.IOException; - -import org.junit.After; -import org.junit.BeforeClass; -import org.junit.Test; - import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponseException; import com.google.api.client.json.JsonFactory; @@ -24,9 +18,16 @@ import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import java.io.IOException; + +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; + public class ApacheHttp2TransportIT { private static final GoogleCredentials MOCK_CREDENTIALS = new MockGoogleCredentials("test_token"); - private static final ImmutableMap payload = ImmutableMap.of("foo", "bar"); + private static final ImmutableMap payload = + ImmutableMap.of("foo", "bar"); // Sets a 1 second delay before response private static final String DELAY_URL = "https://nghttp2.org/httpbin/delay/1"; private static final String POST_URL = "https://nghttp2.org/httpbin/post"; @@ -162,7 +163,8 @@ public void testWriteTimeoutAuthorizedPost() throws FirebaseException { } } - private static ErrorHandlingHttpClient getHttpClient(boolean authorized, FirebaseApp app) { + private static ErrorHandlingHttpClient getHttpClient(boolean authorized, + FirebaseApp app) { HttpRequestFactory requestFactory; if (authorized) { requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); From 81246d9ac2e70d4cab38bf076d5e795c89595330 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Thu, 15 Aug 2024 17:10:27 -0400 Subject: [PATCH 05/21] fix: Timeout tests no longer remove Firebase Apps for other integration tests. --- .../internal/ApacheHttp2TransportIT.java | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index d46fd8e5d..9f53f21a3 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -15,7 +15,6 @@ 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.IOException; @@ -25,9 +24,11 @@ import org.junit.Test; public class ApacheHttp2TransportIT { + private static FirebaseApp app; private static final GoogleCredentials MOCK_CREDENTIALS = new MockGoogleCredentials("test_token"); private static final ImmutableMap payload = ImmutableMap.of("foo", "bar"); + // Sets a 1 second delay before response private static final String DELAY_URL = "https://nghttp2.org/httpbin/delay/1"; private static final String POST_URL = "https://nghttp2.org/httpbin/post"; @@ -38,7 +39,7 @@ public static void setUpClass() { @After public void cleanup() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + app.delete(); } @Test @@ -51,11 +52,11 @@ public void testUnauthorizedPostRequest() throws FirebaseException { @Test public void testConnectTimeoutAuthorizedGet() throws FirebaseException { - FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .setConnectTimeout(1) - .build()); - ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildGetRequest(DELAY_URL); try { @@ -70,11 +71,11 @@ public void testConnectTimeoutAuthorizedGet() throws FirebaseException { @Test public void testConnectTimeoutAuthorizedPost() throws FirebaseException { - FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .setConnectTimeout(1) - .build()); - ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(DELAY_URL, payload); try { @@ -89,11 +90,11 @@ public void testConnectTimeoutAuthorizedPost() throws FirebaseException { @Test public void testReadTimeoutAuthorizedGet() throws FirebaseException { - FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .setReadTimeout(1) - .build()); - ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildGetRequest(DELAY_URL); try { @@ -108,11 +109,11 @@ public void testReadTimeoutAuthorizedGet() throws FirebaseException { @Test public void testReadTimeoutAuthorizedPost() throws FirebaseException { - FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .setReadTimeout(1) - .build()); - ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(DELAY_URL, payload); try { @@ -127,11 +128,11 @@ public void testReadTimeoutAuthorizedPost() throws FirebaseException { @Test public void testWriteTimeoutAuthorizedGet() throws FirebaseException { - FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .setWriteTimeout(1) - .build()); - ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildGetRequest(DELAY_URL); try { @@ -146,11 +147,11 @@ public void testWriteTimeoutAuthorizedGet() throws FirebaseException { @Test public void testWriteTimeoutAuthorizedPost() throws FirebaseException { - FirebaseApp timeoutApp = FirebaseApp.initializeApp(FirebaseOptions.builder() + app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .setWriteTimeout(1) - .build()); - ErrorHandlingHttpClient httpClient = getHttpClient(true, timeoutApp); + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(DELAY_URL, payload); try { @@ -177,9 +178,10 @@ private static ErrorHandlingHttpClient getHttpClient(boolean } private static ErrorHandlingHttpClient getHttpClient(boolean authorized) { - return getHttpClient(authorized, FirebaseApp.initializeApp(FirebaseOptions.builder() + app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - .build(), "test-app")); + .build(), "test-app"); + return getHttpClient(authorized, app); } From ff1e4fb2c0ccf05b263729b3fef675f6814169c1 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Fri, 23 Aug 2024 14:25:37 -0400 Subject: [PATCH 06/21] Mirror tests from google java client and added more descriptive error messages. --- .../ApacheHttp2AsyncEntityProducer.java | 63 ++- .../firebase/internal/ApacheHttp2Request.java | 99 +++-- .../internal/ApacheHttp2Response.java | 44 +- .../internal/ApacheHttp2Transport.java | 35 +- .../internal/MockApacheHttp2AsyncClient.java | 73 ++++ .../internal/ApacheHttp2TransportIT.java | 137 +++++-- .../internal/ApacheHttp2TransportTest.java | 379 ++++++++++++++++++ .../internal/MockApacheHttp2Response.java | 188 +++++++++ 8 files changed, 927 insertions(+), 91 deletions(-) create mode 100644 src/main/java/com/google/firebase/internal/MockApacheHttp2AsyncClient.java create mode 100644 src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java create mode 100644 src/test/java/com/google/firebase/internal/MockApacheHttp2Response.java diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java index 546977239..897f024f4 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java @@ -1,6 +1,23 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.firebase.internal; import com.google.api.client.util.StreamingContent; +import com.google.common.annotations.VisibleForTesting; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -15,8 +32,9 @@ @SuppressWarnings("deprecation") public class ApacheHttp2AsyncEntityProducer implements AsyncEntityProducer { - private final ByteBuffer bytebuf; - private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private ByteBuffer bytebuf; + private ByteArrayOutputStream baos; + private final StreamingContent content; private final ContentType contentType; private final long contentLength; private final String contentEncoding; @@ -25,19 +43,14 @@ public class ApacheHttp2AsyncEntityProducer implements AsyncEntityProducer { public ApacheHttp2AsyncEntityProducer(StreamingContent content, ContentType contentType, String contentEncoding, long contentLength, CompletableFuture writeFuture) { - this.writeFuture = writeFuture; - - if (content != null) { - try { - content.writeTo(baos); - } catch (IOException e) { - writeFuture.completeExceptionally(e); - } - } - this.bytebuf = ByteBuffer.wrap(baos.toByteArray()); + this.content = content; this.contentType = contentType; - this.contentLength = contentLength; this.contentEncoding = contentEncoding; + this.contentLength = contentLength; + this.writeFuture = writeFuture; + this.bytebuf = null; + + this.baos = new ByteArrayOutputStream((int) (contentLength < 0 ? 0 : contentLength)); this.exception = new AtomicReference<>(); } @@ -53,7 +66,7 @@ public ApacheHttp2AsyncEntityProducer(ApacheHttp2Request request, @Override public boolean isRepeatable() { - return false; + return true; } @Override @@ -78,7 +91,7 @@ public String getContentEncoding() { @Override public boolean isChunked() { - return false; + return contentLength == -1; } @Override @@ -88,12 +101,27 @@ public Set getTrailerNames() { @Override public void produce(DataStreamChannel channel) throws IOException { + if (bytebuf == null) { + if (content != null) { + try { + content.writeTo(baos); + } catch (IOException e) { + writeFuture.completeExceptionally(e); + // failed(e); + } + } + + this.bytebuf = ByteBuffer.wrap(baos.toByteArray()); + } + if (bytebuf.hasRemaining()) { channel.write(bytebuf); } + if (!bytebuf.hasRemaining()) { channel.endStream(); writeFuture.complete(null); + releaseResources(); } } @@ -113,4 +141,9 @@ public final Exception getException() { public void releaseResources() { bytebuf.clear(); } + + @VisibleForTesting + ByteBuffer getBytebuf() { + return bytebuf; + } } \ No newline at end of file diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java index 76cc59ad0..3531bd166 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -1,14 +1,34 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.firebase.internal; import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.http.LowLevelHttpResponse; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.apache.hc.client5.http.ConnectTimeoutException; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; @@ -17,6 +37,7 @@ import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http2.H2StreamResetException; import org.apache.hc.core5.util.Timeout; final class ApacheHttp2Request extends LowLevelHttpRequest { @@ -25,6 +46,7 @@ final class ApacheHttp2Request extends LowLevelHttpRequest { private SimpleHttpRequest request; private final RequestConfig.Builder requestConfig; private int writeTimeout; + private ApacheHttp2AsyncEntityProducer entityProducer; ApacheHttp2Request( CloseableHttpAsyncClient httpAsyncClient, SimpleRequestBuilder requestBuilder) { @@ -33,7 +55,7 @@ final class ApacheHttp2Request extends LowLevelHttpRequest { this.writeTimeout = 0; this.requestConfig = RequestConfig.custom() - .setRedirectsEnabled(false); + .setRedirectsEnabled(false); } @Override @@ -42,11 +64,10 @@ public void addHeader(String name, String value) { } @Override - @SuppressWarnings("deprecation") public void setTimeout(int connectionTimeout, int readTimeout) throws IOException { requestConfig - .setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)) - .setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); + .setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)) + .setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); } @Override @@ -64,44 +85,58 @@ public LowLevelHttpResponse execute() throws IOException { // Make Producer CompletableFuture writeFuture = new CompletableFuture<>(); - ApacheHttp2AsyncEntityProducer entityProducer = - new ApacheHttp2AsyncEntityProducer(this, writeFuture); + entityProducer = new ApacheHttp2AsyncEntityProducer(this, writeFuture); // Execute - final CompletableFuture responseFuture = new CompletableFuture<>(); + final Future responseFuture = httpAsyncClient.execute( + new BasicRequestProducer(request, entityProducer), + SimpleResponseConsumer.create(), + new FutureCallback() { + @Override + public void completed(final SimpleHttpResponse response) { + } + + @Override + public void failed(final Exception exception) { + } + + @Override + public void cancelled() { + } + }); + + // Wait for write try { - httpAsyncClient.execute( - new BasicRequestProducer(request, entityProducer), - SimpleResponseConsumer.create(), - new FutureCallback() { - @Override - public void completed(final SimpleHttpResponse response) { - responseFuture.complete(response); - } - - @Override - public void failed(final Exception exception) { - responseFuture.completeExceptionally(exception); - } - - @Override - public void cancelled() { - responseFuture.cancel(false); - } - }); - if (writeTimeout != 0) { writeFuture.get(writeTimeout, TimeUnit.MILLISECONDS); } + } catch (TimeoutException e) { + throw new IOException("Write Timeout", e.getCause()); + } catch (Exception e) { + throw new IOException("Exception in write", e.getCause()); + } + // Wait for response + try { final SimpleHttpResponse response = responseFuture.get(); - return new ApacheHttp2Response(request, response); + return new ApacheHttp2Response(response); + } catch (ExecutionException e) { + if (e.getCause() instanceof ConnectTimeoutException) { + throw new IOException("Connection Timeout", e.getCause()); + } else if (e.getCause() instanceof H2StreamResetException) { + throw new IOException("Stream exception in request", e.getCause()); + } else { + throw new IOException("Exception in request", e); + } } catch (InterruptedException e) { throw new IOException("Request Interrupted", e); - } catch (ExecutionException e) { - throw new IOException("Exception in request", e); - } catch (TimeoutException e) { - throw new IOException("Timed out", e); + } catch (CancellationException e) { + throw new IOException("Request Cancelled", e); } } + + @VisibleForTesting + ApacheHttp2AsyncEntityProducer getEntityProducer() { + return entityProducer; + } } diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java index fd20015ac..4c05b0e03 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java @@ -1,21 +1,37 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.firebase.internal; import com.google.api.client.http.LowLevelHttpResponse; +import com.google.common.annotations.VisibleForTesting; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; public class ApacheHttp2Response extends LowLevelHttpResponse { - private final SimpleHttpResponse response; private final Header[] allHeaders; - ApacheHttp2Response(SimpleHttpRequest request, SimpleHttpResponse response) { + ApacheHttp2Response(SimpleHttpResponse response) { this.response = response; allHeaders = response.getHeaders(); } @@ -33,20 +49,19 @@ public InputStream getContent() throws IOException { @Override public String getContentEncoding() { Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); - if (contentEncodingHeader == null) { - return null; - } - return contentEncodingHeader.getValue(); + return contentEncodingHeader == null ? null : contentEncodingHeader.getValue(); } @Override public long getContentLength() { - return response.getBodyText().length(); + String bodyText = response.getBodyText(); + return bodyText == null ? 0 : bodyText.length(); } @Override public String getContentType() { - return response.getContentType().toString(); + ContentType contentType = response.getContentType(); + return contentType == null ? null : contentType.toString(); } @Override @@ -60,11 +75,7 @@ public String getStatusLine() { } public String getHeaderValue(String name) { - Header header = response.getLastHeader(name); - if (header == null) { - return null; - } - return header.getValue(); + return response.getLastHeader(name).getValue(); } @Override @@ -81,4 +92,9 @@ public int getHeaderCount() { public String getHeaderName(int index) { return allHeaders[index].getName(); } + + @VisibleForTesting + public SimpleHttpResponse getResponse() { + return response; + } } diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java index a94bb7ecd..d24b8ccfd 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.firebase.internal; import com.google.api.client.http.HttpTransport; @@ -16,18 +32,26 @@ import org.apache.hc.core5.http.config.Http1Config; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.util.TimeValue; public final class ApacheHttp2Transport extends HttpTransport { - public final CloseableHttpAsyncClient httpAsyncClient; + private final CloseableHttpAsyncClient httpAsyncClient; + private final boolean isMtls; public ApacheHttp2Transport() { - this(newDefaultHttpAsyncClient()); + this(newDefaultHttpAsyncClient(), false); } public ApacheHttp2Transport(CloseableHttpAsyncClient httpAsyncClient) { + this(httpAsyncClient, false); + } + + public ApacheHttp2Transport(CloseableHttpAsyncClient httpAsyncClient, boolean isMtls) { this.httpAsyncClient = httpAsyncClient; + this.isMtls = isMtls; + httpAsyncClient.start(); } @@ -66,10 +90,15 @@ protected ApacheHttp2Request buildRequest(String method, String url) { @Override public void shutdown() throws IOException { - httpAsyncClient.close(); + httpAsyncClient.close(CloseMode.GRACEFUL); } public HttpAsyncClient getHttpClient() { return httpAsyncClient; } + + @Override + public boolean isMtls() { + return isMtls; + } } diff --git a/src/main/java/com/google/firebase/internal/MockApacheHttp2AsyncClient.java b/src/main/java/com/google/firebase/internal/MockApacheHttp2AsyncClient.java new file mode 100644 index 000000000..c859ec485 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/MockApacheHttp2AsyncClient.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import java.io.IOException; +import java.util.concurrent.Future; + +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.HttpHost; +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.HandlerFactory; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.reactor.IOReactorStatus; +import org.apache.hc.core5.util.TimeValue; + +public class MockApacheHttp2AsyncClient extends CloseableHttpAsyncClient { + + @Override + public void close(CloseMode closeMode) { + } + + @Override + public void close() throws IOException { + } + + @Override + public void start() { + } + + @Override + public IOReactorStatus getStatus() { + return null; + } + + @Override + public void awaitShutdown(TimeValue waitTime) throws InterruptedException { + } + + @Override + public void initiateShutdown() { + } + + @Override + protected Future doExecute(HttpHost target, AsyncRequestProducer requestProducer, + AsyncResponseConsumer responseConsumer, + HandlerFactory pushHandlerFactory, + HttpContext context, FutureCallback callback) { + return null; + } + + @Override + public void register(String hostname, String uriPattern, Supplier supplier) { + } +} diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 9f53f21a3..3c8eb0e57 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -1,11 +1,30 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.firebase.internal; import static org.junit.Assert.assertEquals; +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.HttpRequestFactory; import com.google.api.client.http.HttpResponseException; +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; @@ -16,8 +35,16 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.IncomingHttpResponse; import com.google.firebase.auth.MockGoogleCredentials; - import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +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.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; import org.junit.After; import org.junit.BeforeClass; @@ -29,8 +56,11 @@ public class ApacheHttp2TransportIT { private static final ImmutableMap payload = ImmutableMap.of("foo", "bar"); - // Sets a 1 second delay before response - private static final String DELAY_URL = "https://nghttp2.org/httpbin/delay/1"; + // Connects are not make at port 81 + private static final String NO_CONNECT_URL = "http://google.com:81"; + // Sets a 5 second delay before response + 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"; @BeforeClass @@ -39,10 +69,20 @@ public static void setUpClass() { @After public void cleanup() { - app.delete(); + if (app != null) { + app.delete(); + } + } + + @Test(timeout = 10_000L) + public void testUnauthorizedGetRequest() throws FirebaseException { + ErrorHandlingHttpClient httpClient = getHttpClient(true); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); + IncomingHttpResponse response = httpClient.send(request); + assertEquals(200, response.getStatusCode()); } - @Test + @Test(timeout = 10_000L) public void testUnauthorizedPostRequest() throws FirebaseException { ErrorHandlingHttpClient httpClient = getHttpClient(false); HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(POST_URL, payload); @@ -50,49 +90,49 @@ public void testUnauthorizedPostRequest() throws FirebaseException { assertEquals("{\"foo\":\"bar\"}", body.get("data")); } - @Test + @Test(timeout = 10_000L) public void testConnectTimeoutAuthorizedGet() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - .setConnectTimeout(1) + .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(DELAY_URL); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(NO_CONNECT_URL); try { httpClient.send(request); fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - assertEquals("IO error: Exception in request", e.getMessage()); + assertEquals("IO error: Connection Timeout", e.getMessage()); assertNull(e.getHttpResponse()); } } - @Test + @Test(timeout = 10_000L) public void testConnectTimeoutAuthorizedPost() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - .setConnectTimeout(1) + .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(DELAY_URL, payload); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(NO_CONNECT_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: Exception in request", e.getMessage()); + assertEquals("IO error: Connection Timeout", e.getMessage()); assertNull(e.getHttpResponse()); } } - @Test + @Test(timeout = 10_000L) public void testReadTimeoutAuthorizedGet() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - .setReadTimeout(1) + .setReadTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildGetRequest(DELAY_URL); @@ -102,16 +142,16 @@ public void testReadTimeoutAuthorizedGet() throws FirebaseException { fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - assertEquals("IO error: Exception in request", e.getMessage()); + assertEquals("IO error: Stream exception in request", e.getMessage()); assertNull(e.getHttpResponse()); } } - @Test + @Test(timeout = 10_000L) public void testReadTimeoutAuthorizedPost() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - .setReadTimeout(1) + .setReadTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(DELAY_URL, payload); @@ -121,49 +161,92 @@ public void testReadTimeoutAuthorizedPost() throws FirebaseException { fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - assertEquals("IO error: Exception in request", e.getMessage()); + assertEquals("IO error: Stream exception in request", e.getMessage()); assertNull(e.getHttpResponse()); } } - @Test + @Test(timeout = 10_000L) public void testWriteTimeoutAuthorizedGet() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - .setWriteTimeout(1) + .setWriteTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(DELAY_URL); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(NO_CONNECT_URL); try { httpClient.send(request); fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - assertEquals("IO error: Timed out", e.getMessage()); + assertEquals("IO error: Write Timeout", e.getMessage()); assertNull(e.getHttpResponse()); } } - @Test + @Test(timeout = 10_000L) public void testWriteTimeoutAuthorizedPost() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - .setWriteTimeout(1) + .setWriteTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(DELAY_URL, payload); + 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: Timed out", e.getMessage()); + 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")); + } + + @Test(timeout = 10_000L) + public void testRequestCanSetHeaders() { + final AtomicBoolean interceptorCalled = new AtomicBoolean(false); + CloseableHttpAsyncClient client = HttpAsyncClients.custom() + .addRequestInterceptorFirst( + new HttpRequestInterceptor() { + @Override + public void process( + HttpRequest request, EntityDetails details, HttpContext context) + throws HttpException, IOException { + Header header = request.getFirstHeader("foo"); + assertNotNull("Should have found header", header); + assertEquals("bar", header.getValue()); + interceptorCalled.set(true); + throw new IOException("cancelling request"); + } + }) + .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("Exception in request", exception.getMessage()); + } + assertTrue("Expected to have called our test interceptor", interceptorCalled.get()); + } + private static ErrorHandlingHttpClient getHttpClient(boolean authorized, FirebaseApp app) { HttpRequestFactory requestFactory; diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java new file mode 100644 index 000000000..fcaf563f3 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java @@ -0,0 +1,379 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +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.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.util.ByteArrayStreamingContent; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +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; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.core5.concurrent.FutureCallback; +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.HttpException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestMapper; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.impl.bootstrap.HttpServer; +import org.apache.hc.core5.http.impl.io.HttpService; +import org.apache.hc.core5.http.io.HttpRequestHandler; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.support.BasicHttpServerRequestHandler; +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.HandlerFactory; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.protocol.HttpProcessor; +import org.junit.Assert; +import org.junit.Test; + +public class ApacheHttp2TransportTest { + @Test + public void testContentLengthSet() throws Exception { + SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(HttpMethods.POST) + .setUri("http://www.google.com"); + + ApacheHttp2Request request = new ApacheHttp2Request( + new MockApacheHttp2AsyncClient() { + @SuppressWarnings("unchecked") + @Override + public Future doExecute( + final HttpHost target, + final AsyncRequestProducer requestProducer, + final AsyncResponseConsumer responseConsumer, + final HandlerFactory pushHandlerFactory, + final HttpContext context, + final FutureCallback callback) { + return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + } + }, requestBuilder); + + HttpContent content = new ByteArrayContent("text/plain", + "sample".getBytes(StandardCharsets.UTF_8)); + request.setStreamingContent(content); + request.setContentLength(content.getLength()); + request.execute(); + + assertFalse(request.getEntityProducer().isChunked()); + assertEquals(6, request.getEntityProducer().getContentLength()); + } + + @Test + public void testChunked() throws Exception { + byte[] buf = new byte[300]; + Arrays.fill(buf, (byte) ' '); + SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(HttpMethods.POST) + .setUri("http://www.google.com"); + ApacheHttp2Request request = new ApacheHttp2Request( + new MockApacheHttp2AsyncClient() { + @SuppressWarnings("unchecked") + @Override + public Future doExecute( + final HttpHost target, + final AsyncRequestProducer requestProducer, + final AsyncResponseConsumer responseConsumer, + final HandlerFactory pushHandlerFactory, + final HttpContext context, + final FutureCallback callback) { + return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + } + }, requestBuilder); + + HttpContent content = new InputStreamContent("text/plain", new ByteArrayInputStream(buf)); + request.setStreamingContent(content); + request.execute(); + + assertTrue(request.getEntityProducer().isChunked()); + assertEquals(-1, request.getEntityProducer().getContentLength()); + } + + @Test + public void testExecute() throws Exception { + SimpleHttpResponse simpleHttpResponse = SimpleHttpResponse.create(200, new byte[] { 1, 2, 3 }); + + SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(HttpMethods.POST) + .setUri("http://www.google.com"); + + ApacheHttp2Request request = new ApacheHttp2Request( + new MockApacheHttp2AsyncClient() { + @SuppressWarnings("unchecked") + @Override + public Future doExecute( + final HttpHost target, + final AsyncRequestProducer requestProducer, + final AsyncResponseConsumer responseConsumer, + final HandlerFactory pushHandlerFactory, + final HttpContext context, + final FutureCallback callback) { + return (Future) CompletableFuture.completedFuture(simpleHttpResponse); + } + }, requestBuilder); + LowLevelHttpResponse response = request.execute(); + assertTrue(response instanceof ApacheHttp2Response); + + // we confirm that the simple response we prepared in this test is the same as + // the content's response + assertTrue(response.getContent() instanceof ByteArrayInputStream); + assertEquals(simpleHttpResponse, ((ApacheHttp2Response) response).getResponse()); + // No need to cloase ByteArrayInputStream since close() has no effect. + } + + @Test + public void testApacheHttpTransport() { + ApacheHttp2Transport transport = new ApacheHttp2Transport(); + checkHttpTransport(transport); + assertFalse(transport.isMtls()); + } + + @Test + public void testApacheHttpTransportWithParam() { + HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom(); + ApacheHttp2Transport transport = new ApacheHttp2Transport(clientBuilder.build(), true); + checkHttpTransport(transport); + assertTrue(transport.isMtls()); + } + + @Test + public void testNewDefaultHttpClient() { + HttpAsyncClient client = ApacheHttp2Transport.newDefaultHttpAsyncClient(); + checkHttpClient(client); + } + + @Test + public void testDefaultHttpClientBuilder() { + HttpAsyncClientBuilder clientBuilder = ApacheHttp2Transport.defaultHttpAsyncClientBuilder(); + HttpAsyncClient client = clientBuilder.build(); + checkHttpClient(client); + } + + private void checkHttpTransport(ApacheHttp2Transport transport) { + assertNotNull(transport); + HttpAsyncClient client = transport.getHttpClient(); + checkHttpClient(client); + } + + private void checkHttpClient(HttpAsyncClient client) { + assertNotNull(client); + } + + @Test + public void testRequestsWithContent() throws IOException { + // This test confirms that we can set the content on any type of request + CloseableHttpAsyncClient mockClient = new MockApacheHttp2AsyncClient() { + @SuppressWarnings("unchecked") + @Override + public Future doExecute( + final HttpHost target, + final AsyncRequestProducer requestProducer, + final AsyncResponseConsumer responseConsumer, + final HandlerFactory pushHandlerFactory, + final HttpContext context, + final FutureCallback callback) { + return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + } + }; + ApacheHttp2Transport transport = new ApacheHttp2Transport(mockClient); + + // Test GET. + execute(transport.buildRequest("GET", "http://www.test.url")); + // Test DELETE. + execute(transport.buildRequest("DELETE", "http://www.test.url")); + // Test HEAD. + execute(transport.buildRequest("HEAD", "http://www.test.url")); + // Test PATCH. + execute(transport.buildRequest("PATCH", "http://www.test.url")); + // Test PUT. + execute(transport.buildRequest("PUT", "http://www.test.url")); + // Test POST. + execute(transport.buildRequest("POST", "http://www.test.url")); + // Test PATCH. + execute(transport.buildRequest("PATCH", "http://www.test.url")); + } + + @Test + public void testNormalizedUrl() throws IOException { + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + // Extract the request URI and convert to bytes + byte[] responseData = request.getRequestUri().getBytes(StandardCharsets.UTF_8); + + // Set the response headers (status code and content length) + response.setCode(HttpStatus.SC_OK); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length)); + + // Set the response entity (body) + ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN); + response.setEntity(entity); + } + }; + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); + testUrl.setPort(server.getPort()); + com.google.api.client.http.HttpResponse response = transport.createRequestFactory() + .buildGetRequest(testUrl) + .execute(); + assertEquals(200, response.getStatusCode()); + assertEquals("/foo//bar", response.parseAsString()); + } + } + + @Test + public void testReadErrorStream() throws IOException { + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + byte[] responseData = "Forbidden".getBytes(StandardCharsets.UTF_8); + response.setCode(HttpStatus.SC_FORBIDDEN); // 403 Forbidden + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length)); + ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN); + response.setEntity(entity); + } + }; + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); + testUrl.setPort(server.getPort()); + com.google.api.client.http.HttpRequest getRequest = transport.createRequestFactory() + .buildGetRequest(testUrl); + getRequest.setThrowExceptionOnExecuteError(false); + com.google.api.client.http.HttpResponse response = getRequest.execute(); + assertEquals(403, response.getStatusCode()); + assertEquals("Forbidden", response.parseAsString()); + } + } + + @Test + public void testReadErrorStream_withException() throws IOException { + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + byte[] responseData = "Forbidden".getBytes(StandardCharsets.UTF_8); + response.setCode(HttpStatus.SC_FORBIDDEN); // 403 Forbidden + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length)); + ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN); + response.setEntity(entity); + } + }; + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); + testUrl.setPort(server.getPort()); + com.google.api.client.http.HttpRequest getRequest = transport.createRequestFactory() + .buildGetRequest(testUrl); + try { + getRequest.execute(); + Assert.fail(); + } catch (HttpResponseException ex) { + assertEquals("Forbidden", ex.getContent()); + } + } + } + + private void execute(ApacheHttp2Request request) throws IOException { + byte[] bytes = "abc".getBytes(StandardCharsets.UTF_8); + request.setStreamingContent(new ByteArrayStreamingContent(bytes)); + request.setContentType("text/html"); + request.setContentLength(bytes.length); + request.execute(); + } + + private static class FakeServer implements AutoCloseable { + private final HttpServer server; + + FakeServer(final HttpRequestHandler httpHandler) throws IOException { + HttpRequestMapper mapper = new HttpRequestMapper() { + @Override + public HttpRequestHandler resolve(HttpRequest request, HttpContext context) + throws HttpException { + return httpHandler; + } + }; + server = new HttpServer( + 0, + HttpService.builder() + .withHttpProcessor( + new HttpProcessor() { + @Override + public void process( + HttpRequest request, EntityDetails entity, HttpContext context) + throws HttpException, IOException { + } + + @Override + public void process( + HttpResponse response, EntityDetails entity, HttpContext context) + throws HttpException, IOException { + } + }) + .withHttpServerRequestHandler(new BasicHttpServerRequestHandler(mapper)) + .build(), + null, + null, + null, + null, + null, + null); + server.start(); + } + + public int getPort() { + return server.getLocalPort(); + } + + @Override + public void close() { + server.initiateShutdown(); + } + } +} diff --git a/src/test/java/com/google/firebase/internal/MockApacheHttp2Response.java b/src/test/java/com/google/firebase/internal/MockApacheHttp2Response.java new file mode 100644 index 000000000..0f9c04042 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/MockApacheHttp2Response.java @@ -0,0 +1,188 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.ProtocolVersion; + +public class MockApacheHttp2Response implements HttpResponse { + List
headers = new ArrayList<>(); + int code = 200; + + @Override + public void setVersion(ProtocolVersion version) { + } + + @Override + public ProtocolVersion getVersion() { + return null; + } + + @Override + public void addHeader(Header header) { + headers.add(header); + } + + @Override + public void addHeader(String name, Object value) { + addHeader(newHeader(name, value)); + } + + private Header newHeader(String key, Object value) { + return new Header() { + @Override + public boolean isSensitive() { + return false; + } + + @Override + public String getName() { + return key; + } + + @Override + public String getValue() { + return value.toString(); + } + }; + } + + @Override + public void setHeader(Header header) { + if (headers.contains(header)) { + int index = headers.indexOf(header); + headers.set(index, header); + } else { + addHeader(header); + } + } + + @Override + public void setHeader(String name, Object value) { + setHeader(newHeader(name, value)); + } + + @Override + public void setHeaders(Header... headers) { + for (Header header : headers) { + setHeader(header); + } + } + + @Override + public boolean removeHeader(Header header) { + if (headers.contains(header)) { + headers.remove(headers.indexOf(header)); + return true; + } + return false; + } + + @Override + public boolean removeHeaders(String name) { + int initialSize = headers.size(); + for (Header header : headers.stream().filter(h -> h.getName() == name) + .collect(Collectors.toList())) { + removeHeader(header); + } + return headers.size() < initialSize; + } + + @Override + public boolean containsHeader(String name) { + return headers.stream().anyMatch(h -> h.getName() == name); + } + + @Override + public int countHeaders(String name) { + return headers.size(); + } + + @Override + public Header getFirstHeader(String name) { + return headers.stream().findFirst().orElse(null); + } + + @Override + public Header getHeader(String name) throws ProtocolException { + return headers.stream().filter(h -> h.getName() == name).findFirst().orElse(null); + } + + @Override + public Header[] getHeaders() { + return headers.toArray(new Header[0]); + } + + @Override + public Header[] getHeaders(String name) { + return headers.stream() + .filter(h -> h.getName() == name) + .collect(Collectors.toList()) + .toArray(new Header[0]); + } + + @Override + public Header getLastHeader(String name) { + return headers.isEmpty() ? null : headers.get(headers.size() - 1); + } + + @Override + public Iterator
headerIterator() { + return headers.iterator(); + } + + @Override + public Iterator
headerIterator(String name) { + return headers.stream().filter(h -> h.getName() == name).iterator(); + } + + @Override + public int getCode() { + return this.code; + } + + @Override + public void setCode(int code) { + this.code = code; + } + + @Override + public String getReasonPhrase() { + return null; + } + + @Override + public void setReasonPhrase(String reason) { + } + + @Override + public Locale getLocale() { + return null; + } + + @Override + public void setLocale(Locale loc) { + } +} From 36927d95de6bed1a6ddeb6559b04c493adc86483 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 27 Aug 2024 10:56:36 -0400 Subject: [PATCH 07/21] fix: Remove `NO_CONNECT_URL` --- .../google/firebase/internal/ApacheHttp2TransportIT.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 3c8eb0e57..eb1e1238c 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -56,8 +56,6 @@ public class ApacheHttp2TransportIT { private static final ImmutableMap payload = ImmutableMap.of("foo", "bar"); - // Connects are not make at port 81 - private static final String NO_CONNECT_URL = "http://google.com:81"; // Sets a 5 second delay before response private static final String DELAY_URL = "https://nghttp2.org/httpbin/delay/5"; private static final String GET_URL = "https://nghttp2.org/httpbin/get"; @@ -97,7 +95,7 @@ public void testConnectTimeoutAuthorizedGet() throws FirebaseException { .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(NO_CONNECT_URL); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); try { httpClient.send(request); @@ -116,7 +114,7 @@ public void testConnectTimeoutAuthorizedPost() throws FirebaseException { .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(NO_CONNECT_URL, payload); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(POST_URL, payload); try { httpClient.send(request); @@ -173,7 +171,7 @@ public void testWriteTimeoutAuthorizedGet() throws FirebaseException { .setWriteTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(NO_CONNECT_URL); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); try { httpClient.send(request); From ed88849682d40b85dd3b5d1e446e8950dbbc65bf Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Fri, 30 Aug 2024 13:28:30 -0400 Subject: [PATCH 08/21] debug IT Error --- .../com/google/firebase/internal/ApacheHttp2TransportIT.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index eb1e1238c..b8ab51b3b 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -102,6 +102,11 @@ public void testConnectTimeoutAuthorizedGet() throws FirebaseException { fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + + System.out.println(e.getCause()); + System.out.println(e.getCause().getMessage()); + System.out.println(e.getCause().getCause()); + System.out.println(e.getCause().getCause().getMessage()); assertEquals("IO error: Connection Timeout", e.getMessage()); assertNull(e.getHttpResponse()); } From 21bfa8ac1111ada89c9a6d3324884fb97b5f7f19 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Fri, 30 Aug 2024 15:11:57 -0400 Subject: [PATCH 09/21] debug --- .../firebase/internal/ApacheHttp2TransportIT.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index b8ab51b3b..09b2f6a29 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -95,7 +95,7 @@ public void testConnectTimeoutAuthorizedGet() throws FirebaseException { .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest("http://google.com:81"); try { httpClient.send(request); @@ -107,6 +107,9 @@ public void testConnectTimeoutAuthorizedGet() throws FirebaseException { System.out.println(e.getCause().getMessage()); System.out.println(e.getCause().getCause()); System.out.println(e.getCause().getCause().getMessage()); + System.out.println(e.getCause().getCause().getCause()); + System.out.println(e.getCause().getCause().getCause()); + System.out.println(e.getCause().getCause().getCause().getMessage()); assertEquals("IO error: Connection Timeout", e.getMessage()); assertNull(e.getHttpResponse()); } @@ -119,13 +122,20 @@ public void testConnectTimeoutAuthorizedPost() throws FirebaseException { .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(POST_URL, payload); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest("https://google.com:81", payload); try { httpClient.send(request); fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + + System.out.println(e.getCause()); + System.out.println(e.getCause().getMessage()); + System.out.println(e.getCause().getCause()); + System.out.println(e.getCause().getCause().getMessage()); + System.out.println(e.getCause().getCause().getCause()); + System.out.println(e.getCause().getCause().getCause().getMessage()); assertEquals("IO error: Connection Timeout", e.getMessage()); assertNull(e.getHttpResponse()); } From f9e8576ac7f27de5767626d3224987e84f7acee6 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 3 Sep 2024 11:16:49 -0400 Subject: [PATCH 10/21] debug --- .../internal/ApacheHttp2TransportIT.java | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 09b2f6a29..6710c4577 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -58,6 +58,7 @@ public class ApacheHttp2TransportIT { // Sets a 5 second delay before response private static final String DELAY_URL = "https://nghttp2.org/httpbin/delay/5"; + private static final String NO_CONNECT_URL = "https://google.com:81"; private static final String GET_URL = "https://nghttp2.org/httpbin/get"; private static final String POST_URL = "https://nghttp2.org/httpbin/post"; @@ -92,50 +93,35 @@ public void testUnauthorizedPostRequest() throws FirebaseException { public void testConnectTimeoutAuthorizedGet() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - .setConnectTimeout(100) + // .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest("http://google.com:81"); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(NO_CONNECT_URL); try { httpClient.send(request); fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - - System.out.println(e.getCause()); - System.out.println(e.getCause().getMessage()); - System.out.println(e.getCause().getCause()); - System.out.println(e.getCause().getCause().getMessage()); - System.out.println(e.getCause().getCause().getCause()); - System.out.println(e.getCause().getCause().getCause()); - System.out.println(e.getCause().getCause().getCause().getMessage()); assertEquals("IO error: Connection Timeout", e.getMessage()); assertNull(e.getHttpResponse()); } } - @Test(timeout = 10_000L) + @Test(timeout = 20_000L) public void testConnectTimeoutAuthorizedPost() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - .setConnectTimeout(100) + .setConnectTimeout(5000) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest("https://google.com:81", payload); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(NO_CONNECT_URL, payload); try { httpClient.send(request); fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - - System.out.println(e.getCause()); - System.out.println(e.getCause().getMessage()); - System.out.println(e.getCause().getCause()); - System.out.println(e.getCause().getCause().getMessage()); - System.out.println(e.getCause().getCause().getCause()); - System.out.println(e.getCause().getCause().getCause().getMessage()); assertEquals("IO error: Connection Timeout", e.getMessage()); assertNull(e.getHttpResponse()); } From 3368eb22ea61ef26392198ba48c5b5b533ea9c6d Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 3 Sep 2024 12:41:36 -0400 Subject: [PATCH 11/21] debug --- .../internal/ApacheHttp2TransportIT.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 6710c4577..f5676f833 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -96,33 +96,43 @@ public void testConnectTimeoutAuthorizedGet() throws FirebaseException { // .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(NO_CONNECT_URL); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); try { httpClient.send(request); fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { + System.out.println(e.getCause()); + System.out.println(e.getCause().getMessage()); + System.out.println(e.getCause().getCause()); + System.out.println(e.getCause().getCause().getMessage()); assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); assertEquals("IO error: Connection Timeout", e.getMessage()); assertNull(e.getHttpResponse()); } } - @Test(timeout = 20_000L) + @Test(timeout = 10_000L) public void testConnectTimeoutAuthorizedPost() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - .setConnectTimeout(5000) + .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(NO_CONNECT_URL, payload); + System.out.println(System.getProperty("java.version")); + try { httpClient.send(request); fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { + System.out.println(e.getCause()); + System.out.println(e.getCause().getMessage()); + System.out.println(e.getCause().getCause()); + System.out.println(e.getCause().getCause().getMessage()); assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - assertEquals("IO error: Connection Timeout", e.getMessage()); + assertEquals("IO error: Exception in request", e.getMessage()); assertNull(e.getHttpResponse()); } } From 5797b483a0cc2abb12841d598a885b60dee962b1 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Wed, 4 Sep 2024 12:50:28 -0400 Subject: [PATCH 12/21] debug --- .../internal/ApacheHttp2TransportIT.java | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index f5676f833..75dc62ddd 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -22,8 +22,10 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.client.http.GenericUrl; 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.LowLevelHttpResponse; import com.google.api.client.json.JsonFactory; import com.google.api.client.util.GenericData; @@ -75,7 +77,7 @@ public void cleanup() { @Test(timeout = 10_000L) public void testUnauthorizedGetRequest() throws FirebaseException { - ErrorHandlingHttpClient httpClient = getHttpClient(true); + ErrorHandlingHttpClient httpClient = getHttpClient(false); HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); IncomingHttpResponse response = httpClient.send(request); assertEquals(200, response.getStatusCode()); @@ -89,6 +91,34 @@ public void testUnauthorizedPostRequest() throws FirebaseException { assertEquals("{\"foo\":\"bar\"}", body.get("data")); } + @Test(timeout = 10_000L) + public void testConnectTimeoutGet() throws IOException { + HttpTransport transport = new ApacheHttp2Transport(); + try { + transport.createRequestFactory().buildGetRequest(new GenericUrl(NO_CONNECT_URL)).setConnectTimeout(100).execute(); + fail("No exception thrown for HTTP error response"); + } catch (IOException e) { + System.out.println(e.getCause()); + System.out.println(e.getCause().getMessage()); + assertEquals("Connection Timeout", e.getMessage()); + } + } + + @Test(timeout = 10_000L) + public void testConnectTimeoutPost() throws IOException { + ApacheHttp2Transport transport = new ApacheHttp2Transport(); + ApacheHttp2Request request = transport.buildRequest("POST", NO_CONNECT_URL); + request.setTimeout(100, 0); + try { + request.execute(); + fail("No exception thrown for HTTP error response"); + } catch (IOException e) { + System.out.println(e.getCause()); + System.out.println(e.getCause().getMessage()); + assertEquals("Connection Timeout", e.getMessage()); + } + } + @Test(timeout = 10_000L) public void testConnectTimeoutAuthorizedGet() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() @@ -119,9 +149,7 @@ public void testConnectTimeoutAuthorizedPost() throws FirebaseException { .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(NO_CONNECT_URL, payload); - - System.out.println(System.getProperty("java.version")); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(POST_URL, payload); try { httpClient.send(request); From 0c37ba4152c19085dad770fb18dab88e46c3873f Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Wed, 4 Sep 2024 12:54:39 -0400 Subject: [PATCH 13/21] debug --- .../com/google/firebase/internal/ApacheHttp2TransportIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 75dc62ddd..0ae189c13 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -95,7 +95,8 @@ public void testUnauthorizedPostRequest() throws FirebaseException { public void testConnectTimeoutGet() throws IOException { HttpTransport transport = new ApacheHttp2Transport(); try { - transport.createRequestFactory().buildGetRequest(new GenericUrl(NO_CONNECT_URL)).setConnectTimeout(100).execute(); + transport.createRequestFactory().buildGetRequest(new GenericUrl(NO_CONNECT_URL)) + .setConnectTimeout(100).execute(); fail("No exception thrown for HTTP error response"); } catch (IOException e) { System.out.println(e.getCause()); From 1881f28ec86f877475d26197887eeebe30cb7036 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Thu, 12 Sep 2024 15:31:02 -0400 Subject: [PATCH 14/21] Remove test debug --- .../internal/ApacheHttp2TransportIT.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 0ae189c13..a3a9e5dca 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -60,7 +60,7 @@ public class ApacheHttp2TransportIT { // Sets a 5 second delay before response private static final String DELAY_URL = "https://nghttp2.org/httpbin/delay/5"; - private static final String NO_CONNECT_URL = "https://google.com:81"; + private static final String NO_CONNECT_URL = "http://google.com:81"; private static final String GET_URL = "https://nghttp2.org/httpbin/get"; private static final String POST_URL = "https://nghttp2.org/httpbin/post"; @@ -124,19 +124,15 @@ public void testConnectTimeoutPost() throws IOException { public void testConnectTimeoutAuthorizedGet() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) - // .setConnectTimeout(100) + .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(GET_URL); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(NO_CONNECT_URL); try { httpClient.send(request); fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { - System.out.println(e.getCause()); - System.out.println(e.getCause().getMessage()); - System.out.println(e.getCause().getCause()); - System.out.println(e.getCause().getCause().getMessage()); assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); assertEquals("IO error: Connection Timeout", e.getMessage()); assertNull(e.getHttpResponse()); @@ -150,18 +146,14 @@ public void testConnectTimeoutAuthorizedPost() throws FirebaseException { .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(POST_URL, payload); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(NO_CONNECT_URL, payload); try { httpClient.send(request); fail("No exception thrown for HTTP error response"); } catch (FirebaseException e) { - System.out.println(e.getCause()); - System.out.println(e.getCause().getMessage()); - System.out.println(e.getCause().getCause()); - System.out.println(e.getCause().getCause().getMessage()); assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); - assertEquals("IO error: Exception in request", e.getMessage()); + assertEquals("IO error: Connection Timeout", e.getMessage()); assertNull(e.getHttpResponse()); } } From 74c57b7d7d8c0325f88ad0720dbcd56433849162 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Mon, 16 Sep 2024 10:50:45 -0400 Subject: [PATCH 15/21] Address review comments --- .../ApacheHttp2AsyncEntityProducer.java | 11 +++++---- .../firebase/internal/ApacheHttp2Request.java | 5 +++- .../internal/ApacheHttp2Transport.java | 24 +++++++++++++++---- .../internal/ApacheHttp2TransportIT.java | 5 ++-- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java index 897f024f4..9bdf208c7 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java @@ -30,7 +30,6 @@ import org.apache.hc.core5.http.nio.AsyncEntityProducer; import org.apache.hc.core5.http.nio.DataStreamChannel; -@SuppressWarnings("deprecation") public class ApacheHttp2AsyncEntityProducer implements AsyncEntityProducer { private ByteBuffer bytebuf; private ByteArrayOutputStream baos; @@ -106,8 +105,8 @@ public void produce(DataStreamChannel channel) throws IOException { try { content.writeTo(baos); } catch (IOException e) { - writeFuture.completeExceptionally(e); - // failed(e); + failed(e); + throw e; } } @@ -139,11 +138,13 @@ public final Exception getException() { @Override public void releaseResources() { - bytebuf.clear(); + if (bytebuf != null) { + bytebuf.clear(); + } } @VisibleForTesting ByteBuffer getBytebuf() { return bytebuf; } -} \ No newline at end of file +} diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java index 3531bd166..8e58da934 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeoutException; import org.apache.hc.client5.http.ConnectTimeoutException; +import org.apache.hc.client5.http.HttpHostConnectException; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; @@ -123,10 +124,12 @@ public void cancelled() { } catch (ExecutionException e) { if (e.getCause() instanceof ConnectTimeoutException) { throw new IOException("Connection Timeout", e.getCause()); + } else if (e.getCause() instanceof HttpHostConnectException) { + throw new IOException("Connection exception in request", e.getCause()); } else if (e.getCause() instanceof H2StreamResetException) { throw new IOException("Stream exception in request", e.getCause()); } else { - throw new IOException("Exception in request", e); + throw new IOException("Unknown exception in request", e); } } catch (InterruptedException e) { throw new IOException("Request Interrupted", e); diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java index d24b8ccfd..39cec7c92 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java @@ -24,6 +24,7 @@ import org.apache.hc.client5.http.async.HttpAsyncClient; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; @@ -33,8 +34,10 @@ import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.util.TimeValue; + /** + * HTTP/2 enabled async transport based on the Apache HTTP Client library + */ public final class ApacheHttp2Transport extends HttpTransport { private final CloseableHttpAsyncClient httpAsyncClient; @@ -62,14 +65,19 @@ public static CloseableHttpAsyncClient newDefaultHttpAsyncClient() { public static HttpAsyncClientBuilder defaultHttpAsyncClientBuilder() { PoolingAsyncClientConnectionManager connectionManager = new PoolingAsyncClientConnectionManager(); - connectionManager.setMaxTotal(100); - connectionManager.setDefaultMaxPerRoute(100); - connectionManager.closeIdle(TimeValue.of(30, TimeUnit.SECONDS)); + + // Set Max total connections and max per route to match google api client limits + // https://github.com/googleapis/google-http-java-client/blob/f9d4e15bd3c784b1fd3b0f3468000a91c6f79715/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java#L151 + connectionManager.setMaxTotal(200); + connectionManager.setDefaultMaxPerRoute(20); + connectionManager.setDefaultConnectionConfig( + ConnectionConfig.custom().setTimeToLive(-1, TimeUnit.MILLISECONDS).build()); connectionManager.setDefaultTlsConfig( TlsConfig.custom().setVersionPolicy(HttpVersionPolicy.NEGOTIATE).build()); return HttpAsyncClientBuilder.create() - .setH2Config(H2Config.DEFAULT) + // Set maxConcurrentStreams to 100 to match the concurrent stream limit of the FCM backend. + .setH2Config(H2Config.custom().setMaxConcurrentStreams(100).build()) .setHttp1Config(Http1Config.DEFAULT) .setConnectionManager(connectionManager) .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault())) @@ -88,15 +96,21 @@ protected ApacheHttp2Request buildRequest(String method, String url) { return new ApacheHttp2Request(httpAsyncClient, requestBuilder); } + /** + * Gracefully shuts down the connection manager and releases allocated resources. This closes all + * connections, whether they are currently used or not. + */ @Override public void shutdown() throws IOException { httpAsyncClient.close(CloseMode.GRACEFUL); } + /** Returns the Apache HTTP client. */ public HttpAsyncClient getHttpClient() { return httpAsyncClient; } + /** Returns if the underlying HTTP client is mTLS. */ @Override public boolean isMtls() { return isMtls; diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index a3a9e5dca..38257aa2c 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -58,9 +58,10 @@ public class ApacheHttp2TransportIT { private static final ImmutableMap payload = ImmutableMap.of("foo", "bar"); - // Sets a 5 second delay before response + // 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 NO_CONNECT_URL = "http://google.com:81"; + // Points to a unused port that simulates a slow conncetion which results in a conncetion timeout + private static final String NO_CONNECT_URL = "https://google.com:81"; private static final String GET_URL = "https://nghttp2.org/httpbin/get"; private static final String POST_URL = "https://nghttp2.org/httpbin/post"; From ceac56c7b1b50b9686de4287aa1b07a548c748ed Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 17 Sep 2024 13:19:31 -0400 Subject: [PATCH 16/21] Use local server to test connect timeout and fix lint --- .../internal/ApacheHttp2Transport.java | 6 +-- .../internal/ApacheHttp2TransportIT.java | 41 +++++++++++++++---- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java index 39cec7c92..5b3a091b5 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java @@ -35,9 +35,9 @@ import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.io.CloseMode; - /** - * HTTP/2 enabled async transport based on the Apache HTTP Client library - */ +/** + * HTTP/2 enabled async transport based on the Apache HTTP Client library + */ public final class ApacheHttp2Transport extends HttpTransport { private final CloseableHttpAsyncClient httpAsyncClient; diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 38257aa2c..340cd9736 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -38,6 +38,8 @@ import com.google.firebase.IncomingHttpResponse; import com.google.firebase.auth.MockGoogleCredentials; import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.async.HttpAsyncClients; @@ -49,6 +51,7 @@ import org.apache.hc.core5.http.protocol.HttpContext; import org.junit.After; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -58,17 +61,37 @@ public class ApacheHttp2TransportIT { 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. + // 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"; - // Points to a unused port that simulates a slow conncetion which results in a conncetion timeout - private static final String NO_CONNECT_URL = "https://google.com:81"; 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; + @BeforeClass - public static void setUpClass() { + public static void setUpClass() throws IOException { + // Start server socket with a backlog queue of 1 and a automatically assigned port + serverSocket = new ServerSocket(0, 1); + port = serverSocket.getLocalPort(); + // Fill the backlog queue to force socket to ignore future connections + fillerSocket = new Socket(); + fillerSocket.connect(serverSocket.getLocalSocketAddress()); + } + + @AfterClass + public static void cleanUpClass() throws IOException { + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + } + if (fillerSocket != null && !fillerSocket.isClosed()) { + fillerSocket.close(); + } } + @After public void cleanup() { if (app != null) { @@ -96,7 +119,7 @@ public void testUnauthorizedPostRequest() throws FirebaseException { public void testConnectTimeoutGet() throws IOException { HttpTransport transport = new ApacheHttp2Transport(); try { - transport.createRequestFactory().buildGetRequest(new GenericUrl(NO_CONNECT_URL)) + transport.createRequestFactory().buildGetRequest(new GenericUrl("https://localhost:" + port)) .setConnectTimeout(100).execute(); fail("No exception thrown for HTTP error response"); } catch (IOException e) { @@ -109,7 +132,7 @@ public void testConnectTimeoutGet() throws IOException { @Test(timeout = 10_000L) public void testConnectTimeoutPost() throws IOException { ApacheHttp2Transport transport = new ApacheHttp2Transport(); - ApacheHttp2Request request = transport.buildRequest("POST", NO_CONNECT_URL); + ApacheHttp2Request request = transport.buildRequest("POST", "https://localhost:" + port); request.setTimeout(100, 0); try { request.execute(); @@ -128,7 +151,7 @@ public void testConnectTimeoutAuthorizedGet() throws FirebaseException { .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildGetRequest(NO_CONNECT_URL); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest("https://localhost:" + port); try { httpClient.send(request); @@ -147,7 +170,7 @@ public void testConnectTimeoutAuthorizedPost() throws FirebaseException { .setConnectTimeout(100) .build(), "test-app"); ErrorHandlingHttpClient httpClient = getHttpClient(true, app); - HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(NO_CONNECT_URL, payload); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest("https://localhost:" + port, payload); try { httpClient.send(request); @@ -273,7 +296,7 @@ public void process( request.execute(); fail("should not actually make the request"); } catch (IOException exception) { - assertEquals("Exception in request", exception.getMessage()); + assertEquals("Unknown exception in request", exception.getMessage()); } assertTrue("Expected to have called our test interceptor", interceptorCalled.get()); } From 18365a7b57fd7f89ad016f69a8b352ce60c4be8a Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 17 Sep 2024 13:29:01 -0400 Subject: [PATCH 17/21] Fix testConnectTimeoutGet test --- .../google/firebase/internal/ApacheHttp2TransportIT.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 340cd9736..224eeabe5 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -117,10 +117,11 @@ public void testUnauthorizedPostRequest() throws FirebaseException { @Test(timeout = 10_000L) public void testConnectTimeoutGet() throws IOException { - HttpTransport transport = new ApacheHttp2Transport(); + ApacheHttp2Transport transport = new ApacheHttp2Transport(); + ApacheHttp2Request request = transport.buildRequest("GET", "https://localhost:" + port); + request.setTimeout(100, 0); try { - transport.createRequestFactory().buildGetRequest(new GenericUrl("https://localhost:" + port)) - .setConnectTimeout(100).execute(); + request.execute(); fail("No exception thrown for HTTP error response"); } catch (IOException e) { System.out.println(e.getCause()); From 92d311255df14ed91c953577332b108433b04853 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 17 Sep 2024 14:26:51 -0400 Subject: [PATCH 18/21] Fix lint --- .../com/google/firebase/internal/ApacheHttp2TransportIT.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 224eeabe5..01852d1e0 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -22,10 +22,8 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.api.client.http.GenericUrl; 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.LowLevelHttpResponse; import com.google.api.client.json.JsonFactory; import com.google.api.client.util.GenericData; @@ -117,7 +115,7 @@ public void testUnauthorizedPostRequest() throws FirebaseException { @Test(timeout = 10_000L) public void testConnectTimeoutGet() throws IOException { - ApacheHttp2Transport transport = new ApacheHttp2Transport(); + ApacheHttp2Transport transport = new ApacheHttp2Transport(); ApacheHttp2Request request = transport.buildRequest("GET", "https://localhost:" + port); request.setTimeout(100, 0); try { From 73e1355b3079c9290bee95ba948eda569ee1c7a0 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 17 Sep 2024 14:35:54 -0400 Subject: [PATCH 19/21] remove deplicate tests --- .../internal/ApacheHttp2TransportIT.java | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 01852d1e0..9301969de 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -113,36 +113,6 @@ public void testUnauthorizedPostRequest() throws FirebaseException { assertEquals("{\"foo\":\"bar\"}", body.get("data")); } - @Test(timeout = 10_000L) - public void testConnectTimeoutGet() throws IOException { - ApacheHttp2Transport transport = new ApacheHttp2Transport(); - ApacheHttp2Request request = transport.buildRequest("GET", "https://localhost:" + port); - request.setTimeout(100, 0); - try { - request.execute(); - fail("No exception thrown for HTTP error response"); - } catch (IOException e) { - System.out.println(e.getCause()); - System.out.println(e.getCause().getMessage()); - assertEquals("Connection Timeout", e.getMessage()); - } - } - - @Test(timeout = 10_000L) - public void testConnectTimeoutPost() throws IOException { - ApacheHttp2Transport transport = new ApacheHttp2Transport(); - ApacheHttp2Request request = transport.buildRequest("POST", "https://localhost:" + port); - request.setTimeout(100, 0); - try { - request.execute(); - fail("No exception thrown for HTTP error response"); - } catch (IOException e) { - System.out.println(e.getCause()); - System.out.println(e.getCause().getMessage()); - assertEquals("Connection Timeout", e.getMessage()); - } - } - @Test(timeout = 10_000L) public void testConnectTimeoutAuthorizedGet() throws FirebaseException { app = FirebaseApp.initializeApp(FirebaseOptions.builder() From 165ff592b6a50e4c4748e518d89ff805fc82a6b4 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 17 Sep 2024 14:58:33 -0400 Subject: [PATCH 20/21] fix: catch `java.net.SocketTimeoutException` --- .../java/com/google/firebase/internal/ApacheHttp2Request.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java index 8e58da934..56c2c9034 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -21,6 +21,7 @@ import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.net.SocketTimeoutException; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -122,7 +123,8 @@ public void cancelled() { final SimpleHttpResponse response = responseFuture.get(); return new ApacheHttp2Response(response); } catch (ExecutionException e) { - if (e.getCause() instanceof ConnectTimeoutException) { + if (e.getCause() instanceof ConnectTimeoutException + || e.getCause() instanceof SocketTimeoutException) { throw new IOException("Connection Timeout", e.getCause()); } else if (e.getCause() instanceof HttpHostConnectException) { throw new IOException("Connection exception in request", e.getCause()); From 11aab962a9e6022ef8cba4d9874c0f3e2333e103 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Mon, 23 Sep 2024 15:28:12 -0400 Subject: [PATCH 21/21] Proxy tests --- pom.xml | 5 + .../internal/ApacheHttp2Transport.java | 27 +++-- .../internal/ApacheHttp2TransportIT.java | 112 ++++++++++++++++-- 3 files changed, 126 insertions(+), 18 deletions(-) diff --git a/pom.xml b/pom.xml index 2e7a09a3b..4a8e039d8 100644 --- a/pom.xml +++ b/pom.xml @@ -493,5 +493,10 @@ 2.2 test + + org.mock-server + mockserver-junit-rule-no-dependencies + 5.14.0 + diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java index 5b3a091b5..9c0e413fb 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Transport.java @@ -29,11 +29,14 @@ import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; import org.apache.hc.core5.http.config.Http1Config; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.ssl.SSLContexts; /** * HTTP/2 enabled async transport based on the Apache HTTP Client library @@ -64,16 +67,20 @@ public static CloseableHttpAsyncClient newDefaultHttpAsyncClient() { public static HttpAsyncClientBuilder defaultHttpAsyncClientBuilder() { PoolingAsyncClientConnectionManager connectionManager = - new PoolingAsyncClientConnectionManager(); - - // Set Max total connections and max per route to match google api client limits - // https://github.com/googleapis/google-http-java-client/blob/f9d4e15bd3c784b1fd3b0f3468000a91c6f79715/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java#L151 - connectionManager.setMaxTotal(200); - connectionManager.setDefaultMaxPerRoute(20); - connectionManager.setDefaultConnectionConfig( - ConnectionConfig.custom().setTimeToLive(-1, TimeUnit.MILLISECONDS).build()); - connectionManager.setDefaultTlsConfig( - TlsConfig.custom().setVersionPolicy(HttpVersionPolicy.NEGOTIATE).build()); + PoolingAsyncClientConnectionManagerBuilder.create() + // Set Max total connections to match google api client limits + // https://github.com/googleapis/google-http-java-client/blob/f9d4e15bd3c784b1fd3b0f3468000a91c6f79715/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java#L151 + .setMaxConnTotal(200) + // Set max connections per route to match the concurrent stream limit of the FCM backend. + .setMaxConnPerRoute(100) + .setDefaultConnectionConfig( + ConnectionConfig.custom().setTimeToLive(-1, TimeUnit.MILLISECONDS).build()) + .setDefaultTlsConfig( + TlsConfig.custom().setVersionPolicy(HttpVersionPolicy.NEGOTIATE).build()) + .setTlsStrategy(ClientTlsStrategyBuilder.create() + .setSslContext(SSLContexts.createSystemDefault()) + .build()) + .build(); return HttpAsyncClientBuilder.create() // Set maxConcurrentStreams to 100 to match the concurrent stream limit of the FCM backend. diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java index 9301969de..c8fbe5533 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportIT.java @@ -21,9 +21,15 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockserver.model.Header.header; +import static org.mockserver.model.HttpForward.forward; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import com.google.api.client.http.GenericUrl; 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.LowLevelHttpResponse; import com.google.api.client.json.JsonFactory; import com.google.api.client.util.GenericData; @@ -53,13 +59,17 @@ import org.junit.BeforeClass; import org.junit.Test; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpForward.Scheme; +import org.mockserver.socket.PortFactory; + public class ApacheHttp2TransportIT { private static FirebaseApp app; private static final GoogleCredentials MOCK_CREDENTIALS = new MockGoogleCredentials("test_token"); private static final ImmutableMap payload = ImmutableMap.of("foo", "bar"); - // Sets a 5 second delay before server response to simulate a slow network that + // 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"; @@ -69,9 +79,13 @@ public class ApacheHttp2TransportIT { private static Socket fillerSocket; private static int port; + private static ClientAndServer mockProxy; + private static ClientAndServer mockServer; + @BeforeClass public static void setUpClass() throws IOException { - // Start server socket with a backlog queue of 1 and a automatically assigned port + // Start server socket with a backlog queue of 1 and a automatically assigned + // port serverSocket = new ServerSocket(0, 1); port = serverSocket.getLocalPort(); // Fill the backlog queue to force socket to ignore future connections @@ -89,14 +103,26 @@ public static void cleanUpClass() throws IOException { } } - @After public void cleanup() { if (app != null) { app.delete(); } + + if (mockProxy != null && mockProxy.isRunning()) { + mockProxy.close(); + } + + if (mockServer != null && mockServer.isRunning()) { + mockServer.close(); + } + + System.clearProperty("http.proxyHost"); + System.clearProperty("http.proxyPort"); + System.clearProperty("https.proxyHost"); + System.clearProperty("https.proxyPort"); } - + @Test(timeout = 10_000L) public void testUnauthorizedGetRequest() throws FirebaseException { ErrorHandlingHttpClient httpClient = getHttpClient(false); @@ -115,7 +141,7 @@ public void testUnauthorizedPostRequest() throws FirebaseException { @Test(timeout = 10_000L) public void testConnectTimeoutAuthorizedGet() throws FirebaseException { - app = FirebaseApp.initializeApp(FirebaseOptions.builder() + app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(MOCK_CREDENTIALS) .setConnectTimeout(100) .build(), "test-app"); @@ -270,6 +296,77 @@ public void process( 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"); + + HttpTransport transport = new ApacheHttp2Transport(); + transport.createRequestFactory().buildGetRequest(new GenericUrl(GET_URL)).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")); + } + } + + @Test(timeout = 10_000L) + public void testProxyMockHttp() throws Exception { + // Start MockServer + mockProxy = ClientAndServer.startClientAndServer(PortFactory.findFreePort()); + mockServer = ClientAndServer.startClientAndServer(PortFactory.findFreePort()); + + System.setProperty("http.proxyHost", "localhost"); + System.setProperty("http.proxyPort", mockProxy.getPort().toString()); + + // Configure proxy to receieve requests and forward them to a mock destination + // server + mockProxy + .when( + request()) + .forward( + forward() + .withHost("localhost") + .withPort(mockServer.getPort()) + .withScheme(Scheme.HTTP)); + + // Configure server to listen and respond + mockServer + .when( + request()) + .respond( + response() + .withStatusCode(200) + .withBody("Expected server response")); + + // Send a request through the proxy + app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setWriteTimeout(100) + .build(), "test-app"); + ErrorHandlingHttpClient httpClient = getHttpClient(true, app); + HttpRequestInfo request = HttpRequestInfo.buildGetRequest("http://www.google.com"); + IncomingHttpResponse response = httpClient.send(request); + + // Verify that the proxy received request with destination host + mockProxy.verify( + request() + .withMethod("GET") + .withPath("/") + .withHeader(header("Host", "www.google.com"))); + + // Verify the forwarded request is received by the server + mockServer.verify( + request() + .withMethod("GET") + .withPath("/")); + + // Verify response + assertEquals(200, response.getStatusCode()); + assertEquals(response.getContent(), "Expected server response"); + } + private static ErrorHandlingHttpClient getHttpClient(boolean authorized, FirebaseApp app) { HttpRequestFactory requestFactory; @@ -285,12 +382,11 @@ private static ErrorHandlingHttpClient getHttpClient(boolean private static ErrorHandlingHttpClient getHttpClient(boolean authorized) { app = FirebaseApp.initializeApp(FirebaseOptions.builder() - .setCredentials(MOCK_CREDENTIALS) - .build(), "test-app"); + .setCredentials(MOCK_CREDENTIALS) + .build(), "test-app"); return getHttpClient(authorized, app); } - private static class TestHttpErrorHandler implements HttpErrorHandler { @Override public FirebaseException handleIOException(IOException e) {