Skip to content

Commit

Permalink
feat: Add HTTP/2 enabled transport as default transport (#979)
Browse files Browse the repository at this point in the history
* Added HTTP/2 enabled transport and made it default.

* fix: Use internal default transport

* Added test coverage for timeouts

* fix: lint

* fix: Timeout tests no longer remove Firebase Apps for other integration tests.

* Mirror tests from google java client and added more descriptive error messages.

* fix: Remove `NO_CONNECT_URL`

* debug IT Error

* debug

* debug

* debug

* debug

* debug

* Remove test debug

* Address review comments

* Use local server to test connect timeout and fix lint

* Fix testConnectTimeoutGet test

* Fix lint

* remove deplicate tests

* fix: catch `java.net.SocketTimeoutException`

* Proxy tests
  • Loading branch information
jonathanedey authored Oct 4, 2024
1 parent ce8e7fd commit ab46aaa
Show file tree
Hide file tree
Showing 14 changed files with 1,590 additions and 9 deletions.
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,11 @@
<artifactId>netty-transport</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>

<!-- Test Dependencies -->
<dependency>
Expand Down Expand Up @@ -488,5 +493,10 @@
<version>3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-junit-rule-no-dependencies</artifactId>
<version>5.14.0</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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;
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;

public class ApacheHttp2AsyncEntityProducer implements AsyncEntityProducer {
private ByteBuffer bytebuf;
private ByteArrayOutputStream baos;
private final StreamingContent content;
private final ContentType contentType;
private final long contentLength;
private final String contentEncoding;
private final CompletableFuture<Void> writeFuture;
private final AtomicReference<Exception> exception;

public ApacheHttp2AsyncEntityProducer(StreamingContent content, ContentType contentType,
String contentEncoding, long contentLength, CompletableFuture<Void> writeFuture) {
this.content = content;
this.contentType = contentType;
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<>();
}

public ApacheHttp2AsyncEntityProducer(ApacheHttp2Request request,
CompletableFuture<Void> writeFuture) {
this(
request.getStreamingContent(),
ContentType.parse(request.getContentType()),
request.getContentEncoding(),
request.getContentLength(),
writeFuture);
}

@Override
public boolean isRepeatable() {
return true;
}

@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 contentLength == -1;
}

@Override
public Set<String> getTrailerNames() {
return null;
}

@Override
public void produce(DataStreamChannel channel) throws IOException {
if (bytebuf == null) {
if (content != null) {
try {
content.writeTo(baos);
} catch (IOException e) {
failed(e);
throw e;
}
}

this.bytebuf = ByteBuffer.wrap(baos.toByteArray());
}

if (bytebuf.hasRemaining()) {
channel.write(bytebuf);
}

if (!bytebuf.hasRemaining()) {
channel.endStream();
writeFuture.complete(null);
releaseResources();
}
}

@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() {
if (bytebuf != null) {
bytebuf.clear();
}
}

@VisibleForTesting
ByteBuffer getBytebuf() {
return bytebuf;
}
}
147 changes: 147 additions & 0 deletions src/main/java/com/google/firebase/internal/ApacheHttp2Request.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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.net.SocketTimeoutException;
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.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;
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.http2.H2StreamResetException;
import org.apache.hc.core5.util.Timeout;

final class ApacheHttp2Request extends LowLevelHttpRequest {
private final CloseableHttpAsyncClient httpAsyncClient;
private final SimpleRequestBuilder requestBuilder;
private SimpleHttpRequest request;
private final RequestConfig.Builder requestConfig;
private int writeTimeout;
private ApacheHttp2AsyncEntityProducer entityProducer;

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<Void> writeFuture = new CompletableFuture<>();
entityProducer = new ApacheHttp2AsyncEntityProducer(this, writeFuture);

// Execute
final Future<SimpleHttpResponse> responseFuture = httpAsyncClient.execute(
new BasicRequestProducer(request, entityProducer),
SimpleResponseConsumer.create(),
new FutureCallback<SimpleHttpResponse>() {
@Override
public void completed(final SimpleHttpResponse response) {
}

@Override
public void failed(final Exception exception) {
}

@Override
public void cancelled() {
}
});

// Wait for write
try {
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(response);
} catch (ExecutionException e) {
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());
} else if (e.getCause() instanceof H2StreamResetException) {
throw new IOException("Stream exception in request", e.getCause());
} else {
throw new IOException("Unknown exception in request", e);
}
} catch (InterruptedException e) {
throw new IOException("Request Interrupted", e);
} catch (CancellationException e) {
throw new IOException("Request Cancelled", e);
}
}

@VisibleForTesting
ApacheHttp2AsyncEntityProducer getEntityProducer() {
return entityProducer;
}
}
Loading

0 comments on commit ab46aaa

Please sign in to comment.