blob: b82ad06bb4b957861e473752fd300568700793b7 [file] [log] [blame]
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.core5.testing.compatibility.http2;
import java.io.IOException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Message;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.config.CharCodingConfig;
import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester;
import org.apache.hc.core5.http.message.BasicHttpRequest;
import org.apache.hc.core5.http.nio.AsyncClientEndpoint;
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer;
import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
import org.apache.hc.core5.http.nio.support.AbstractAsyncPushHandler;
import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
import org.apache.hc.core5.http2.HttpVersionPolicy;
import org.apache.hc.core5.http2.config.H2Config;
import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.reactor.IOReactorConfig;
import org.apache.hc.core5.testing.classic.LoggingConnPoolListener;
import org.apache.hc.core5.testing.nio.LoggingExceptionCallback;
import org.apache.hc.core5.testing.nio.LoggingH2StreamListener;
import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener;
import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator;
import org.apache.hc.core5.testing.nio.LoggingIOSessionListener;
import org.apache.hc.core5.util.TextUtils;
import org.apache.hc.core5.util.Timeout;
public class H2CompatibilityTest {
private final HttpAsyncRequester client;
public static void main(final String... args) throws Exception {
final HttpHost[] h2servers = new HttpHost[]{
new HttpHost("http", "localhost", 8080),
new HttpHost("http", "localhost", 8081)
};
final HttpHost httpbin = new HttpHost("http", "localhost", 8082);
final H2CompatibilityTest test = new H2CompatibilityTest();
try {
test.start();
for (final HttpHost h2server : h2servers) {
test.executeH2(h2server);
}
test.executeHttpBin(httpbin);
} finally {
test.shutdown();
}
}
H2CompatibilityTest() throws Exception {
this.client = H2RequesterBootstrap.bootstrap()
.setIOReactorConfig(IOReactorConfig.custom()
.setSoTimeout(TIMEOUT)
.build())
.setH2Config(H2Config.custom()
.setPushEnabled(true)
.build())
.setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
.setStreamListener(LoggingH2StreamListener.INSTANCE)
.setConnPoolListener(LoggingConnPoolListener.INSTANCE)
.setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
.setExceptionCallback(LoggingExceptionCallback.INSTANCE)
.setIOSessionListener(LoggingIOSessionListener.INSTANCE)
.create();
}
void start() throws Exception {
client.start();
}
void shutdown() throws Exception {
client.close(CloseMode.GRACEFUL);
}
private static final Timeout TIMEOUT = Timeout.ofSeconds(5);
void executeH2(final HttpHost target) throws Exception {
{
System.out.println("*** HTTP/2 simple request execution ***");
final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null);
try {
final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
final CountDownLatch countDownLatch = new CountDownLatch(1);
final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/status.html");
endpoint.execute(
new BasicRequestProducer(httpget, null),
new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
new FutureCallback<Message<HttpResponse, String>>() {
@Override
public void completed(final Message<HttpResponse, String> responseMessage) {
final HttpResponse response = responseMessage.getHead();
final int code = response.getCode();
if (code == HttpStatus.SC_OK) {
logResult(TestResult.OK, target, httpget, response,
Objects.toString(response.getFirstHeader("server")));
} else {
logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")");
}
countDownLatch.countDown();
}
@Override
public void failed(final Exception ex) {
logResult(TestResult.NOK, target, httpget, null, "(" + ex.getMessage() + ")");
countDownLatch.countDown();
}
@Override
public void cancelled() {
logResult(TestResult.NOK, target, httpget, null, "(cancelled)");
countDownLatch.countDown();
}
});
if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
logResult(TestResult.NOK, target, null, null, "(single request execution failed to complete in time)");
}
} catch (final ExecutionException ex) {
final Throwable cause = ex.getCause();
logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")");
} catch (final TimeoutException ex) {
logResult(TestResult.NOK, target, null, null, "(time out)");
}
}
{
System.out.println("*** HTTP/2 multiplexed request execution ***");
final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null);
try {
final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
final int reqCount = 20;
final CountDownLatch countDownLatch = new CountDownLatch(reqCount);
for (int i = 0; i < reqCount; i++) {
final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/status.html");
endpoint.execute(
new BasicRequestProducer(httpget, null),
new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
new FutureCallback<Message<HttpResponse, String>>() {
@Override
public void completed(final Message<HttpResponse, String> responseMessage) {
final HttpResponse response = responseMessage.getHead();
final int code = response.getCode();
if (code == HttpStatus.SC_OK) {
logResult(TestResult.OK, target, httpget, response,
"multiplexed / " + response.getFirstHeader("server"));
} else {
logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")");
}
countDownLatch.countDown();
}
@Override
public void failed(final Exception ex) {
logResult(TestResult.NOK, target, httpget, null, "(" + ex.getMessage() + ")");
countDownLatch.countDown();
}
@Override
public void cancelled() {
logResult(TestResult.NOK, target, httpget, null, "(cancelled)");
countDownLatch.countDown();
}
});
}
if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
logResult(TestResult.NOK, target, null, null, "(multiplexed request execution failed to complete in time)");
}
} catch (final ExecutionException ex) {
final Throwable cause = ex.getCause();
logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")");
} catch (final TimeoutException ex) {
logResult(TestResult.NOK, target, null, null, "(time out)");
}
}
{
System.out.println("*** HTTP/2 request execution with push ***");
final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null);
try {
final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
final CountDownLatch countDownLatch = new CountDownLatch(5);
final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/index.html");
final Future<Message<HttpResponse, String>> future = endpoint.execute(
new BasicRequestProducer(httpget, null),
new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
(request, context) -> new AbstractAsyncPushHandler<Message<HttpResponse, Void>>(
new BasicResponseConsumer<>(new DiscardingEntityConsumer<>())) {
@Override
protected void handleResponse(
final HttpRequest promise,
final Message<HttpResponse, Void> responseMessage) throws IOException, HttpException {
final HttpResponse response = responseMessage.getHead();
logResult(TestResult.OK, target, promise, response,
"pushed / " + response.getFirstHeader("server"));
countDownLatch.countDown();
}
@Override
protected void handleError(
final HttpRequest promise,
final Exception cause) {
logResult(TestResult.NOK, target, promise, null, "(" + cause.getMessage() + ")");
countDownLatch.countDown();
}
},
null,
null);
final Message<HttpResponse, String> message = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
final HttpResponse response = message.getHead();
final int code = response.getCode();
if (code == HttpStatus.SC_OK) {
logResult(TestResult.OK, target, httpget, response,
Objects.toString(response.getFirstHeader("server")));
if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
logResult(TestResult.NOK, target, null, null, "Push messages not received");
}
} else {
logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")");
}
} catch (final ExecutionException ex) {
final Throwable cause = ex.getCause();
logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")");
} catch (final TimeoutException ex) {
logResult(TestResult.NOK, target, null, null, "(time out)");
}
}
}
void executeHttpBin(final HttpHost target) throws Exception {
{
System.out.println("*** httpbin.org HTTP/1.1 simple request execution ***");
final List<Message<HttpRequest, AsyncEntityProducer>> requestMessages = Arrays.asList(
new Message<>(
new BasicHttpRequest(Method.GET, target, "/headers"),
null),
new Message<>(
new BasicHttpRequest(Method.POST, target, "/anything"),
new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
new Message<>(
new BasicHttpRequest(Method.PUT, target, "/anything"),
new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
new Message<>(
new BasicHttpRequest(Method.GET, target, "/drip"),
null),
new Message<>(
new BasicHttpRequest(Method.GET, target, "/bytes/20000"),
null),
new Message<>(
new BasicHttpRequest(Method.GET, target, "/delay/2"),
null),
new Message<>(
new BasicHttpRequest(Method.POST, target, "/delay/2"),
new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
new Message<>(
new BasicHttpRequest(Method.PUT, target, "/delay/2"),
new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN))
);
for (final Message<HttpRequest, AsyncEntityProducer> message : requestMessages) {
final CountDownLatch countDownLatch = new CountDownLatch(1);
final HttpRequest request = message.getHead();
final AsyncEntityProducer entityProducer = message.getBody();
client.execute(
new BasicRequestProducer(request, entityProducer),
new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom()
.setCharset(StandardCharsets.US_ASCII)
.setMalformedInputAction(CodingErrorAction.IGNORE)
.setUnmappableInputAction(CodingErrorAction.REPLACE)
.build())),
TIMEOUT,
new FutureCallback<Message<HttpResponse, String>>() {
@Override
public void completed(final Message<HttpResponse, String> responseMessage) {
final HttpResponse response = responseMessage.getHead();
final int code = response.getCode();
if (code == HttpStatus.SC_OK) {
logResult(TestResult.OK, target, request, response,
Objects.toString(response.getFirstHeader("server")));
} else {
logResult(TestResult.NOK, target, request, response, "(status " + code + ")");
}
countDownLatch.countDown();
}
@Override
public void failed(final Exception ex) {
logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")");
countDownLatch.countDown();
}
@Override
public void cancelled() {
logResult(TestResult.NOK, target, request, null, "(cancelled)");
countDownLatch.countDown();
}
});
if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)");
}
}
}
{
System.out.println("*** httpbin.org HTTP/1.1 pipelined request execution ***");
final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT);
final AsyncClientEndpoint streamEndpoint = connectFuture.get();
final int n = 10;
final CountDownLatch countDownLatch = new CountDownLatch(n);
for (int i = 0; i < n; i++) {
final HttpRequest request;
final AsyncEntityProducer entityProducer;
if (i % 2 == 0) {
request = new BasicHttpRequest(Method.GET, target, "/headers");
entityProducer = null;
} else {
request = new BasicHttpRequest(Method.POST, target, "/anything");
entityProducer = new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN);
}
streamEndpoint.execute(
new BasicRequestProducer(request, entityProducer),
new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom()
.setCharset(StandardCharsets.US_ASCII)
.setMalformedInputAction(CodingErrorAction.IGNORE)
.setUnmappableInputAction(CodingErrorAction.REPLACE)
.build())),
new FutureCallback<Message<HttpResponse, String>>() {
@Override
public void completed(final Message<HttpResponse, String> responseMessage) {
final HttpResponse response = responseMessage.getHead();
final int code = response.getCode();
if (code == HttpStatus.SC_OK) {
logResult(TestResult.OK, target, request, response,
"pipelined / " + response.getFirstHeader("server"));
} else {
logResult(TestResult.NOK, target, request, response, "(status " + code + ")");
}
countDownLatch.countDown();
}
@Override
public void failed(final Exception ex) {
logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")");
countDownLatch.countDown();
}
@Override
public void cancelled() {
logResult(TestResult.NOK, target, request, null, "(cancelled)");
countDownLatch.countDown();
}
});
}
if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)");
}
}
}
enum TestResult {OK, NOK}
private void logResult(
final TestResult result,
final HttpHost httpHost,
final HttpRequest request,
final HttpResponse response,
final String message) {
final StringBuilder buf = new StringBuilder();
buf.append(result);
if (buf.length() == 2) {
buf.append(" ");
}
buf.append(": ").append(httpHost).append(" ");
if (response != null) {
buf.append(response.getVersion()).append(" ");
}
if (request != null) {
buf.append(request.getMethod()).append(" ").append(request.getRequestUri());
}
if (message != null && !TextUtils.isBlank(message)) {
buf.append(" -> ").append(message);
}
System.out.println(buf);
}
}