Skip to content

Commit ab46aaa

Browse files
authored
feat: Add HTTP/2 enabled transport as default transport (#979)
* 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
1 parent ce8e7fd commit ab46aaa

14 files changed

+1590
-9
lines changed

pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,11 @@
455455
<artifactId>netty-transport</artifactId>
456456
<version>${netty.version}</version>
457457
</dependency>
458+
<dependency>
459+
<groupId>org.apache.httpcomponents.client5</groupId>
460+
<artifactId>httpclient5</artifactId>
461+
<version>5.3.1</version>
462+
</dependency>
458463

459464
<!-- Test Dependencies -->
460465
<dependency>
@@ -488,5 +493,10 @@
488493
<version>3.0</version>
489494
<scope>test</scope>
490495
</dependency>
496+
<dependency>
497+
<groupId>org.mock-server</groupId>
498+
<artifactId>mockserver-junit-rule-no-dependencies</artifactId>
499+
<version>5.14.0</version>
500+
</dependency>
491501
</dependencies>
492502
</project>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2024 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.internal;
18+
19+
import com.google.api.client.util.StreamingContent;
20+
import com.google.common.annotations.VisibleForTesting;
21+
22+
import java.io.ByteArrayOutputStream;
23+
import java.io.IOException;
24+
import java.nio.ByteBuffer;
25+
import java.util.Set;
26+
import java.util.concurrent.CompletableFuture;
27+
import java.util.concurrent.atomic.AtomicReference;
28+
29+
import org.apache.hc.core5.http.ContentType;
30+
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
31+
import org.apache.hc.core5.http.nio.DataStreamChannel;
32+
33+
public class ApacheHttp2AsyncEntityProducer implements AsyncEntityProducer {
34+
private ByteBuffer bytebuf;
35+
private ByteArrayOutputStream baos;
36+
private final StreamingContent content;
37+
private final ContentType contentType;
38+
private final long contentLength;
39+
private final String contentEncoding;
40+
private final CompletableFuture<Void> writeFuture;
41+
private final AtomicReference<Exception> exception;
42+
43+
public ApacheHttp2AsyncEntityProducer(StreamingContent content, ContentType contentType,
44+
String contentEncoding, long contentLength, CompletableFuture<Void> writeFuture) {
45+
this.content = content;
46+
this.contentType = contentType;
47+
this.contentEncoding = contentEncoding;
48+
this.contentLength = contentLength;
49+
this.writeFuture = writeFuture;
50+
this.bytebuf = null;
51+
52+
this.baos = new ByteArrayOutputStream((int) (contentLength < 0 ? 0 : contentLength));
53+
this.exception = new AtomicReference<>();
54+
}
55+
56+
public ApacheHttp2AsyncEntityProducer(ApacheHttp2Request request,
57+
CompletableFuture<Void> writeFuture) {
58+
this(
59+
request.getStreamingContent(),
60+
ContentType.parse(request.getContentType()),
61+
request.getContentEncoding(),
62+
request.getContentLength(),
63+
writeFuture);
64+
}
65+
66+
@Override
67+
public boolean isRepeatable() {
68+
return true;
69+
}
70+
71+
@Override
72+
public String getContentType() {
73+
return contentType != null ? contentType.toString() : null;
74+
}
75+
76+
@Override
77+
public long getContentLength() {
78+
return contentLength;
79+
}
80+
81+
@Override
82+
public int available() {
83+
return Integer.MAX_VALUE;
84+
}
85+
86+
@Override
87+
public String getContentEncoding() {
88+
return contentEncoding;
89+
}
90+
91+
@Override
92+
public boolean isChunked() {
93+
return contentLength == -1;
94+
}
95+
96+
@Override
97+
public Set<String> getTrailerNames() {
98+
return null;
99+
}
100+
101+
@Override
102+
public void produce(DataStreamChannel channel) throws IOException {
103+
if (bytebuf == null) {
104+
if (content != null) {
105+
try {
106+
content.writeTo(baos);
107+
} catch (IOException e) {
108+
failed(e);
109+
throw e;
110+
}
111+
}
112+
113+
this.bytebuf = ByteBuffer.wrap(baos.toByteArray());
114+
}
115+
116+
if (bytebuf.hasRemaining()) {
117+
channel.write(bytebuf);
118+
}
119+
120+
if (!bytebuf.hasRemaining()) {
121+
channel.endStream();
122+
writeFuture.complete(null);
123+
releaseResources();
124+
}
125+
}
126+
127+
@Override
128+
public void failed(Exception cause) {
129+
if (exception.compareAndSet(null, cause)) {
130+
releaseResources();
131+
writeFuture.completeExceptionally(cause);
132+
}
133+
}
134+
135+
public final Exception getException() {
136+
return exception.get();
137+
}
138+
139+
@Override
140+
public void releaseResources() {
141+
if (bytebuf != null) {
142+
bytebuf.clear();
143+
}
144+
}
145+
146+
@VisibleForTesting
147+
ByteBuffer getBytebuf() {
148+
return bytebuf;
149+
}
150+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright 2024 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.internal;
18+
19+
import com.google.api.client.http.LowLevelHttpRequest;
20+
import com.google.api.client.http.LowLevelHttpResponse;
21+
import com.google.common.annotations.VisibleForTesting;
22+
23+
import java.io.IOException;
24+
import java.net.SocketTimeoutException;
25+
import java.util.concurrent.CancellationException;
26+
import java.util.concurrent.CompletableFuture;
27+
import java.util.concurrent.ExecutionException;
28+
import java.util.concurrent.Future;
29+
import java.util.concurrent.TimeUnit;
30+
import java.util.concurrent.TimeoutException;
31+
32+
import org.apache.hc.client5.http.ConnectTimeoutException;
33+
import org.apache.hc.client5.http.HttpHostConnectException;
34+
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
35+
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
36+
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
37+
import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer;
38+
import org.apache.hc.client5.http.config.RequestConfig;
39+
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
40+
import org.apache.hc.core5.concurrent.FutureCallback;
41+
import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
42+
import org.apache.hc.core5.http2.H2StreamResetException;
43+
import org.apache.hc.core5.util.Timeout;
44+
45+
final class ApacheHttp2Request extends LowLevelHttpRequest {
46+
private final CloseableHttpAsyncClient httpAsyncClient;
47+
private final SimpleRequestBuilder requestBuilder;
48+
private SimpleHttpRequest request;
49+
private final RequestConfig.Builder requestConfig;
50+
private int writeTimeout;
51+
private ApacheHttp2AsyncEntityProducer entityProducer;
52+
53+
ApacheHttp2Request(
54+
CloseableHttpAsyncClient httpAsyncClient, SimpleRequestBuilder requestBuilder) {
55+
this.httpAsyncClient = httpAsyncClient;
56+
this.requestBuilder = requestBuilder;
57+
this.writeTimeout = 0;
58+
59+
this.requestConfig = RequestConfig.custom()
60+
.setRedirectsEnabled(false);
61+
}
62+
63+
@Override
64+
public void addHeader(String name, String value) {
65+
requestBuilder.addHeader(name, value);
66+
}
67+
68+
@Override
69+
public void setTimeout(int connectionTimeout, int readTimeout) throws IOException {
70+
requestConfig
71+
.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout))
72+
.setResponseTimeout(Timeout.ofMilliseconds(readTimeout));
73+
}
74+
75+
@Override
76+
public void setWriteTimeout(int writeTimeout) throws IOException {
77+
this.writeTimeout = writeTimeout;
78+
}
79+
80+
@Override
81+
public LowLevelHttpResponse execute() throws IOException {
82+
// Set request configs
83+
requestBuilder.setRequestConfig(requestConfig.build());
84+
85+
// Build request
86+
request = requestBuilder.build();
87+
88+
// Make Producer
89+
CompletableFuture<Void> writeFuture = new CompletableFuture<>();
90+
entityProducer = new ApacheHttp2AsyncEntityProducer(this, writeFuture);
91+
92+
// Execute
93+
final Future<SimpleHttpResponse> responseFuture = httpAsyncClient.execute(
94+
new BasicRequestProducer(request, entityProducer),
95+
SimpleResponseConsumer.create(),
96+
new FutureCallback<SimpleHttpResponse>() {
97+
@Override
98+
public void completed(final SimpleHttpResponse response) {
99+
}
100+
101+
@Override
102+
public void failed(final Exception exception) {
103+
}
104+
105+
@Override
106+
public void cancelled() {
107+
}
108+
});
109+
110+
// Wait for write
111+
try {
112+
if (writeTimeout != 0) {
113+
writeFuture.get(writeTimeout, TimeUnit.MILLISECONDS);
114+
}
115+
} catch (TimeoutException e) {
116+
throw new IOException("Write Timeout", e.getCause());
117+
} catch (Exception e) {
118+
throw new IOException("Exception in write", e.getCause());
119+
}
120+
121+
// Wait for response
122+
try {
123+
final SimpleHttpResponse response = responseFuture.get();
124+
return new ApacheHttp2Response(response);
125+
} catch (ExecutionException e) {
126+
if (e.getCause() instanceof ConnectTimeoutException
127+
|| e.getCause() instanceof SocketTimeoutException) {
128+
throw new IOException("Connection Timeout", e.getCause());
129+
} else if (e.getCause() instanceof HttpHostConnectException) {
130+
throw new IOException("Connection exception in request", e.getCause());
131+
} else if (e.getCause() instanceof H2StreamResetException) {
132+
throw new IOException("Stream exception in request", e.getCause());
133+
} else {
134+
throw new IOException("Unknown exception in request", e);
135+
}
136+
} catch (InterruptedException e) {
137+
throw new IOException("Request Interrupted", e);
138+
} catch (CancellationException e) {
139+
throw new IOException("Request Cancelled", e);
140+
}
141+
}
142+
143+
@VisibleForTesting
144+
ApacheHttp2AsyncEntityProducer getEntityProducer() {
145+
return entityProducer;
146+
}
147+
}

0 commit comments

Comments
 (0)