I am trying to implement a SdkAsyncHttpClient that uses Java 11's java.net.http.HttpClient (specifically sendAsync
). SdkAsyncHttpClient has one method to implement CompletableFuture<Void> execute(AsyncExecuteRequest asyncExecuteRequest)
. The AsyncExecuteRequest
provides a way to get details about the HTTP request and, crucially, a SdkHttpContentPublisher
. This goes into the paradigm of a reactive Publisher/Subscribe model - which HttpClient.sendAsync
seems to have builtin support for. I seem to be close to an implementation but (at least) one crucial step is missing as I can't seem to get the returned future to ever be completed.
I think that I am probably missing something fundamental to link the two together in a straight-forward way but so far it eludes me.
Here is my attempt at a naive (and very simple) implementation:
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import software.amazon.awssdk.http.Protocol;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
import software.amazon.awssdk.http.async.SdkHttpContentPublisher;
import software.amazon.awssdk.utils.AttributeMap;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Flow;
import static java.net.http.HttpClient.Version.HTTP_1_1;
import static java.net.http.HttpClient.Version.HTTP_2;
import static software.amazon.awssdk.http.Protocol.HTTP2;
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.CONNECTION_TIMEOUT;
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.PROTOCOL;
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.READ_TIMEOUT;
public class JavaAsyncHttpClient implements SdkAsyncHttpClient {
private final HttpClient httpClient;
public JavaAsyncHttpClient(AttributeMap options) {
this.httpClient = HttpClient.newBuilder()
.connectTimeout(options.get(CONNECTION_TIMEOUT))
.version(options.get(PROTOCOL) == HTTP2 ? HTTP_2 : HTTP_1_1)
.build();
}
@Override
public CompletableFuture<Void> execute(AsyncExecuteRequest asyncExecuteRequest) {
SdkHttpRequest request = asyncExecuteRequest.request();
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(request.getUri());
for (Map.Entry<String, List<String>> header : request.headers().entrySet()) {
// avoid java.lang.IllegalArgumentException: restricted header name: "Content-Length"
if (!header.getKey().equalsIgnoreCase("Content-Length") && !header.getKey().equalsIgnoreCase("Host")) {
for (String headerVal : header.getValue()) {
requestBuilder = requestBuilder.header(header.getKey(), headerVal);
}
}
}
switch (request.method()) {
case POST:
requestBuilder = requestBuilder.POST(HttpRequest.BodyPublishers.fromPublisher(
toFlowPublisher(asyncExecuteRequest.requestContentPublisher())));
break;
case PUT:
requestBuilder = requestBuilder.PUT(HttpRequest.BodyPublishers.fromPublisher(
toFlowPublisher(asyncExecuteRequest.requestContentPublisher())));
break;
case DELETE:
requestBuilder = requestBuilder.DELETE();
break;
case HEAD:
requestBuilder = requestBuilder.method("HEAD", HttpRequest.BodyPublishers.noBody());
break;
case PATCH:
throw new UnsupportedOperationException("PATCH not supported");
case OPTIONS:
requestBuilder = requestBuilder.method("OPTIONS", HttpRequest.BodyPublishers.noBody());
break;
}
// Need to use BodyHandlers.ofPublisher() or is that a dead end? How can link up the AWS Publisher/Subscribers
Subscriber<ByteBuffer> subscriber = new BaosSubscriber(new CompletableFuture<>());
asyncExecuteRequest.requestContentPublisher().subscribe(subscriber);
HttpRequest httpRequest = requestBuilder.build();
return httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.fromSubscriber(toFlowSubscriber(subscriber)))
.thenApply(voidHttpResponse -> null);
}
private Flow.Subscriber<? super List<ByteBuffer>> toFlowSubscriber(Subscriber<ByteBuffer> subscriber) {
return new Flow.Subscriber<>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscriber.onSubscribe(toAwsSubscription(subscription));
}
@Override
public void onNext(List<ByteBuffer> item) {
subscriber.onNext(item.get(0));
}
@Override
public void onError(Throwable throwable) {
subscriber.onError(throwable);
}
@Override
public void onComplete() {
subscriber.onComplete();
}
};
}
private Subscription toAwsSubscription(Flow.Subscription subscription) {
return new Subscription() {
@Override
public void request(long n) {
subscription.request(n);
}
@Override
public void cancel() {
subscription.cancel();
}
};
}
private Flow.Publisher<ByteBuffer> toFlowPublisher(SdkHttpContentPublisher requestContentPublisher) {
return subscriber -> requestContentPublisher.subscribe(toAwsSubscriber(subscriber));
}
private Subscriber<? super ByteBuffer> toAwsSubscriber(Flow.Subscriber<? super ByteBuffer> subscriber) {
return new Subscriber<>() {
@Override
public void onSubscribe(Subscription s) {
subscriber.onSubscribe(toFlowSubscription(s));
}
@Override
public void onNext(ByteBuffer byteBuffer) {
subscriber.onNext(byteBuffer);
}
@Override
public void onError(Throwable t) {
subscriber.onError(t);
}
@Override
public void onComplete() {
subscriber.onComplete();
}
};
}
private Flow.Subscription toFlowSubscription(Subscription subscription) {
return new Flow.Subscription() {
@Override
public void request(long n) {
subscription.request(n);
}
@Override
public void cancel() {
subscription.cancel();
}
};
}
@Override
public void close() {}
private static class BaosSubscriber implements Subscriber<ByteBuffer> {
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
private final CompletableFuture<ByteArrayOutputStream> streamFuture;
private Subscription subscription;
private BaosSubscriber(CompletableFuture<ByteArrayOutputStream> streamFuture) {
this.streamFuture = streamFuture;
}
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(ByteBuffer byteBuffer) {
try {
baos.write(BinaryUtils.copyBytesFrom(byteBuffer));
this.subscription.request(Long.MAX_VALUE);
} catch (IOException e) {
// Should never happen
streamFuture.completeExceptionally(e);
}
}
@Override
public void onError(Throwable t) {
streamFuture.completeExceptionally(t);
}
@Override
public void onComplete() {
streamFuture.complete(baos);
}
}
What am I missing here? Returning a future that completes with null
is following the spec of SdkAsyncHttpClient
so clearly the HTTP response needs to somehow be sent to a subscriber on the AWS side of things - but that's where I get lost.
Edit: Just found this via Googling: https://github.com/rmcsoft/j11_aws_http_client/blob/63f05326990317c59f1863be55942054769b437e/src/main/java/com/rmcsoft/aws/http/proxy/BodyHandlerProxy.java - going to see if the answers lie within.