-
Notifications
You must be signed in to change notification settings - Fork 269
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
ce8e7fd
commit ab46aaa
Showing
14 changed files
with
1,590 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
150 changes: 150 additions & 0 deletions
150
src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityProducer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
147
src/main/java/com/google/firebase/internal/ApacheHttp2Request.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.