/*
* Copyright (c) 2011-2013 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.vertx.test.core;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http2.AbstractHttp2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.DefaultHttp2Headers;
import io.netty.handler.codec.http2.Http2CodecUtil;
import io.netty.handler.codec.http2.Http2ConnectionDecoder;
import io.netty.handler.codec.http2.Http2ConnectionEncoder;
import io.netty.handler.codec.http2.Http2ConnectionHandler;
import io.netty.handler.codec.http2.Http2Error;
import io.netty.handler.codec.http2.Http2EventAdapter;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.codec.http2.Http2FrameListener;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.AsciiString;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Context;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Verticle;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpConnection;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.http.StreamResetException;
import io.vertx.core.impl.VertxInternal;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.SocketAddress;
import io.vertx.core.net.impl.SSLHelper;
import io.vertx.test.core.tls.Cert;
import io.vertx.test.core.tls.Trust;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.zip.GZIPOutputStream;
import static io.vertx.test.core.TestUtils.*;
/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
*/
public class Http2ClientTest extends Http2TestBase {
private List<EventLoopGroup> eventLoopGroups = new ArrayList<>();
@Override
protected void tearDown() throws Exception {
super.tearDown();
for (EventLoopGroup eventLoopGroup : eventLoopGroups) {
eventLoopGroup.shutdownGracefully(0, 10, TimeUnit.SECONDS);
}
}
@Override
public void setUp() throws Exception {
eventLoopGroups.clear();
super.setUp();
clientOptions = new HttpClientOptions().
setUseAlpn(true).
setSsl(true).
setTrustStoreOptions(Trust.SERVER_JKS.get()).
setProtocolVersion(HttpVersion.HTTP_2);
client = vertx.createHttpClient(clientOptions);
}
@Test
public void testClientSettings() throws Exception {
waitFor(2);
io.vertx.core.http.Http2Settings initialSettings = TestUtils.randomHttp2Settings();
io.vertx.core.http.Http2Settings updatedSettings = TestUtils.randomHttp2Settings();
updatedSettings.setHeaderTableSize(initialSettings.getHeaderTableSize()); // Otherwise it raise "invalid max dynamic table size" in Netty
AtomicInteger count = new AtomicInteger();
Future<Void> end = Future.future();
server.requestHandler(req -> {
end.setHandler(v -> {
req.response().end();
});
}).connectionHandler(conn -> {
Context ctx = Vertx.currentContext();
conn.remoteSettingsHandler(settings -> {
assertOnIOContext(ctx);
switch (count.getAndIncrement()) {
case 0:
assertEquals(initialSettings.isPushEnabled(), settings.isPushEnabled());
assertEquals(initialSettings.getMaxHeaderListSize(), settings.getMaxHeaderListSize());
assertEquals(initialSettings.getMaxFrameSize(), settings.getMaxFrameSize());
assertEquals(initialSettings.getInitialWindowSize(), settings.getInitialWindowSize());
// assertEquals(Math.min(initialSettings.getMaxConcurrentStreams(), Integer.MAX_VALUE), settings.getMaxConcurrentStreams());
assertEquals(initialSettings.getHeaderTableSize(), settings.getHeaderTableSize());
assertEquals(initialSettings.get('\u0007'), settings.get(7));
break;
case 1:
// find out why it fails sometimes ...
// assertEquals(updatedSettings.pushEnabled(), settings.getEnablePush());
assertEquals(updatedSettings.getMaxHeaderListSize(), settings.getMaxHeaderListSize());
assertEquals(updatedSettings.getMaxFrameSize(), settings.getMaxFrameSize());
assertEquals(updatedSettings.getInitialWindowSize(), settings.getInitialWindowSize());
// find out why it fails sometimes ...
// assertEquals(Math.min(updatedSettings.maxConcurrentStreams(), Integer.MAX_VALUE), settings.getMaxConcurrentStreams());
assertEquals(updatedSettings.getHeaderTableSize(), settings.getHeaderTableSize());
assertEquals(updatedSettings.get('\u0007'), settings.get(7));
complete();
break;
}
});
});
startServer();
client.close();
client = vertx.createHttpClient(clientOptions.setInitialSettings(initialSettings));
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
complete();
}).exceptionHandler(this::fail).connectionHandler(conn -> {
vertx.runOnContext(v -> {
conn.updateSettings(updatedSettings, ar -> {
end.complete();
});
});
}).end();
await();
}
@Test
public void testServerSettings() throws Exception {
waitFor(2);
io.vertx.core.http.Http2Settings expectedSettings = TestUtils.randomHttp2Settings();
expectedSettings.setHeaderTableSize((int)io.vertx.core.http.Http2Settings.DEFAULT_HEADER_TABLE_SIZE);
server.close();
server = vertx.createHttpServer(serverOptions);
Context otherContext = vertx.getOrCreateContext();
server.connectionHandler(conn -> {
otherContext.runOnContext(v -> {
conn.updateSettings(expectedSettings);
});
});
server.requestHandler(req -> {
req.response().end();
});
startServer();
AtomicInteger count = new AtomicInteger();
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
complete();
}).connectionHandler(conn -> {
conn.remoteSettingsHandler(settings -> {
switch (count.getAndIncrement()) {
case 0:
// Initial settings
break;
case 1:
assertEquals(expectedSettings.getMaxHeaderListSize(), settings.getMaxHeaderListSize());
assertEquals(expectedSettings.getMaxFrameSize(), settings.getMaxFrameSize());
assertEquals(expectedSettings.getInitialWindowSize(), settings.getInitialWindowSize());
assertEquals(expectedSettings.getMaxConcurrentStreams(), settings.getMaxConcurrentStreams());
assertEquals(expectedSettings.getHeaderTableSize(), settings.getHeaderTableSize());
assertEquals(expectedSettings.get('\u0007'), settings.get(7));
complete();
break;
}
});
}).end();
await();
}
@Test
public void testGet() throws Exception {
ServerBootstrap bootstrap = createH2Server((decoder, encoder) -> new Http2EventAdapter() {
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
vertx.runOnContext(v -> {
assertTrue(endStream);
encoder.writeHeaders(ctx, streamId, new DefaultHttp2Headers().status("200"), 0, true, ctx.newPromise());
ctx.flush();
});
}
@Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) throws Http2Exception {
vertx.runOnContext(v -> {
testComplete();
});
}
});
ChannelFuture s = bootstrap.bind(DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT).sync();
try {
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath");
req.handler(resp -> {
Context ctx = vertx.getOrCreateContext();
assertOnIOContext(ctx);
resp.endHandler(v -> {
assertOnIOContext(ctx);
req.connection().close();
});
}).end();
await();
} finally {
s.channel().close().sync();
}
}
@Test
public void testHeaders() throws Exception {
AtomicInteger reqCount = new AtomicInteger();
server.requestHandler(req -> {
assertEquals("https", req.scheme());
assertEquals(HttpMethod.GET, req.method());
assertEquals("/somepath", req.path());
assertEquals(DEFAULT_HTTPS_HOST_AND_PORT, req.host());
assertEquals("foo_request_value", req.getHeader("Foo_request"));
assertEquals("bar_request_value", req.getHeader("bar_request"));
assertEquals(2, req.headers().getAll("juu_request").size());
assertEquals("juu_request_value_1", req.headers().getAll("juu_request").get(0));
assertEquals("juu_request_value_2", req.headers().getAll("juu_request").get(1));
reqCount.incrementAndGet();
HttpServerResponse resp = req.response();
resp.putHeader("content-type", "text/plain");
resp.putHeader("Foo_response", "foo_value");
resp.putHeader("bar_response", "bar_value");
resp.putHeader("juu_response", (List<String>) Arrays.asList("juu_value_1", "juu_value_2"));
resp.end();
});
startServer();
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath");
req.handler(resp -> {
Context ctx = vertx.getOrCreateContext();
assertOnIOContext(ctx);
assertEquals(3, req.streamId());
assertEquals(1, reqCount.get());
assertEquals(HttpVersion.HTTP_2, resp.version());
assertEquals(200, resp.statusCode());
assertEquals("OK", resp.statusMessage());
assertEquals("text/plain", resp.getHeader("content-type"));
assertEquals("200", resp.getHeader(":status"));
assertEquals("foo_value", resp.getHeader("foo_response"));
assertEquals("bar_value", resp.getHeader("bar_response"));
assertEquals(2, resp.headers().getAll("juu_response").size());
assertEquals("juu_value_1", resp.headers().getAll("juu_response").get(0));
assertEquals("juu_value_2", resp.headers().getAll("juu_response").get(1));
resp.endHandler(v -> {
assertOnIOContext(ctx);
testComplete();
});
})
.putHeader("Foo_request", "foo_request_value")
.putHeader("bar_request", "bar_request_value")
.putHeader("juu_request", Arrays.<CharSequence>asList("juu_request_value_1", "juu_request_value_2"))
.exceptionHandler(err -> fail())
.end();
await();
}
@Test
public void testResponseBody() throws Exception {
testResponseBody(TestUtils.randomAlphaString(100));
}
@Test
public void testEmptyResponseBody() throws Exception {
testResponseBody("");
}
private void testResponseBody(String expected) throws Exception {
server.requestHandler(req -> {
HttpServerResponse resp = req.response();
resp.end(expected);
});
startServer();
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
AtomicInteger count = new AtomicInteger();
Buffer content = Buffer.buffer();
resp.handler(buff -> {
content.appendBuffer(buff);
count.incrementAndGet();
});
resp.endHandler(v -> {
assertTrue(count.get() > 0);
assertEquals(expected, content.toString());
testComplete();
});
})
.exceptionHandler(err -> fail())
.end();
await();
}
@Test
public void testOverrideAuthority() throws Exception {
server.requestHandler(req -> {
assertEquals("localhost:4444", req.host());
req.response().end();
});
startServer();
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
testComplete();
})
.setHost("localhost:4444")
.exceptionHandler(this::fail)
.end();
await();
}
@Test
public void testTrailers() throws Exception {
server.requestHandler(req -> {
HttpServerResponse resp = req.response();
resp.setChunked(true);
resp.write("some-content");
resp.putTrailer("Foo", "foo_value");
resp.putTrailer("bar", "bar_value");
resp.putTrailer("juu", (List<String>)Arrays.asList("juu_value_1", "juu_value_2"));
resp.end();
});
startServer();
client.getNow(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepeth", resp -> {
assertEquals(null, resp.getTrailer("foo"));
resp.exceptionHandler(this::fail);
resp.endHandler(v -> {
assertEquals("foo_value", resp.getTrailer("foo"));
assertEquals("foo_value", resp.getTrailer("Foo"));
assertEquals("bar_value", resp.getTrailer("bar"));
assertEquals(2, resp.trailers().getAll("juu").size());
assertEquals("juu_value_1", resp.trailers().getAll("juu").get(0));
assertEquals("juu_value_2", resp.trailers().getAll("juu").get(1));
testComplete();
});
});
await();
}
@Test
public void testBodyEndHandler() throws Exception {
// Large body so it will be fragmented in several HTTP2 data frames
Buffer expected = Buffer.buffer(TestUtils.randomAlphaString(128 * 1024));
server.requestHandler(req -> {
HttpServerResponse resp = req.response();
resp.end(expected);
});
startServer();
client.getNow(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
Context ctx = vertx.getOrCreateContext();
resp.exceptionHandler(this::fail);
resp.bodyHandler(body -> {
assertOnIOContext(ctx);
assertEquals(expected, body);
testComplete();
});
});
await();
}
@Test
public void testPost() throws Exception {
testPost(TestUtils.randomAlphaString(100));
}
@Test
public void testEmptyPost() throws Exception {
testPost("");
}
private void testPost(String expected) throws Exception {
Buffer content = Buffer.buffer();
AtomicInteger count = new AtomicInteger();
server.requestHandler(req -> {
assertEquals(HttpMethod.POST, req.method());
req.handler(buff -> {
content.appendBuffer(buff);
count.getAndIncrement();
});
req.endHandler(v -> {
assertTrue(count.get() > 0);
req.response().end();
});
});
startServer();
client.post(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
resp.endHandler(v -> {
assertEquals(expected, content.toString());
testComplete();
});
}).exceptionHandler(err -> {
fail();
}).end(expected);
await();
}
@Test
public void testClientRequestWriteability() throws Exception {
Buffer content = Buffer.buffer();
Buffer expected = Buffer.buffer();
String chunk = TestUtils.randomAlphaString(100);
CompletableFuture<Void> done = new CompletableFuture<>();
AtomicBoolean paused = new AtomicBoolean();
AtomicInteger numPause = new AtomicInteger();
server.requestHandler(req -> {
Context ctx = vertx.getOrCreateContext();
done.thenAccept(v1 -> {
paused.set(false);
ctx.runOnContext(v2 -> {
req.resume();
});
});
numPause.incrementAndGet();
req.pause();
paused.set(true);
req.handler(content::appendBuffer);
req.endHandler(v -> {
assertEquals(expected, content);
req.response().end();
});
});
startServer();
HttpClientRequest req = client.post(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
testComplete();
}).setChunked(true).exceptionHandler(err -> {
fail();
});
AtomicInteger sent = new AtomicInteger();
AtomicInteger count = new AtomicInteger();
AtomicInteger drained = new AtomicInteger();
vertx.setPeriodic(1, timerID -> {
Context ctx = vertx.getOrCreateContext();
if (req.writeQueueFull()) {
assertTrue(paused.get());
assertEquals(1, numPause.get());
req.drainHandler(v -> {
assertOnIOContext(ctx);
assertEquals(0, drained.getAndIncrement());
assertEquals(1, numPause.get());
assertFalse(paused.get());
req.end();
});
vertx.cancelTimer(timerID);
done.complete(null);
} else {
count.incrementAndGet();
expected.appendString(chunk);
req.write(chunk);
sent.addAndGet(chunk.length());
}
});
await();
}
@Test
public void testClientResponsePauseResume() throws Exception {
String content = TestUtils.randomAlphaString(1024);
Buffer expected = Buffer.buffer();
Future<Void> whenFull = Future.future();
AtomicBoolean drain = new AtomicBoolean();
server.requestHandler(req -> {
HttpServerResponse resp = req.response();
resp.putHeader("content-type", "text/plain");
resp.setChunked(true);
vertx.setPeriodic(1, timerID -> {
if (resp.writeQueueFull()) {
resp.drainHandler(v -> {
Buffer last = Buffer.buffer("last");
expected.appendBuffer(last);
resp.end(last);
assertEquals(expected.toString().getBytes().length, resp.bytesWritten());
});
vertx.cancelTimer(timerID);
drain.set(true);
whenFull.complete();
} else {
Buffer chunk = Buffer.buffer(content);
expected.appendBuffer(chunk);
resp.write(chunk);
}
});
});
startServer();
client.getNow(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
Context ctx = vertx.getOrCreateContext();
Buffer received = Buffer.buffer();
resp.pause();
resp.handler(buff -> {
if (whenFull.isComplete()) {
assertSame(ctx, Vertx.currentContext());
} else {
assertOnIOContext(ctx);
}
received.appendBuffer(buff);
});
resp.endHandler(v -> {
assertEquals(expected.toString().length(), received.toString().length());
testComplete();
});
whenFull.setHandler(v -> {
resp.resume();
});
});
await();
}
@Test
public void testQueueingRequests() throws Exception {
testQueueingRequests(100, null);
}
@Test
public void testQueueingRequestsMaxConcurrentStream() throws Exception {
testQueueingRequests(100, 10L);
}
private void testQueueingRequests(int numReq, Long max) throws Exception {
waitFor(numReq);
String expected = TestUtils.randomAlphaString(100);
server.close();
io.vertx.core.http.Http2Settings serverSettings = new io.vertx.core.http.Http2Settings();
if (max != null) {
serverSettings.setMaxConcurrentStreams(max);
}
server = vertx.createHttpServer(serverOptions.setInitialSettings(serverSettings));
server.requestHandler(req -> {
req.response().end(expected);
});
startServer();
CountDownLatch latch = new CountDownLatch(1);
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
}).connectionHandler(conn -> {
conn.remoteSettingsHandler(settings -> {
assertEquals(max == null ? 0xFFFFFFFFL : max, settings.getMaxConcurrentStreams());
latch.countDown();
});
}).exceptionHandler(err -> {
fail();
}).end();
awaitLatch(latch);
for (int i = 0;i < numReq;i++) {
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
Buffer content = Buffer.buffer();
resp.handler(content::appendBuffer);
resp.endHandler(v -> {
assertEquals(expected, content.toString());
complete();
});
}).exceptionHandler(err -> {
fail();
}).end();
}
await();
}
@Test
public void testReuseConnection() throws Exception {
List<SocketAddress> ports = new ArrayList<>();
server.requestHandler(req -> {
SocketAddress address = req.remoteAddress();
assertNotNull(address);
ports.add(address);
req.response().end();
});
startServer();
CountDownLatch doReq = new CountDownLatch(1);
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
resp.endHandler(v -> {
doReq.countDown();
});
}).exceptionHandler(err -> {
fail();
}).end();
awaitLatch(doReq);
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
resp.endHandler(v -> {
assertEquals(2, ports.size());
assertEquals(ports.get(0), ports.get(1));
testComplete();
});
}).exceptionHandler(err -> {
fail();
}).end();
await();
}
@Test
public void testConnectionFailed() throws Exception {
client.get(4044, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
}).exceptionHandler(err -> {
Context ctx = Vertx.currentContext();
assertOnIOContext(ctx);
assertTrue(err instanceof ConnectException);
testComplete();
}).end();
await();
}
@Test
public void testFallbackOnHttp1() throws Exception {
server.close();
server = vertx.createHttpServer(serverOptions.setUseAlpn(false));
server.requestHandler(req -> {
assertEquals(HttpVersion.HTTP_1_1, req.version());
req.response().end();
});
startServer();
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
testComplete();
}).exceptionHandler(this::fail).end();
await();
}
@Test
public void testServerResetClientStreamDuringRequest() throws Exception {
String chunk = TestUtils.randomAlphaString(1024);
server.requestHandler(req -> {
req.handler(buf -> {
req.response().reset(8);
});
});
startServer();
client.post(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
fail();
}).exceptionHandler(err -> {
Context ctx = Vertx.currentContext();
assertOnIOContext(ctx);
assertTrue(err instanceof StreamResetException);
StreamResetException reset = (StreamResetException) err;
assertEquals(8, reset.getCode());
testComplete();
}).setChunked(true).write(chunk);
await();
}
@Test
public void testServerResetClientStreamDuringResponse() throws Exception {
waitFor(2);
String chunk = TestUtils.randomAlphaString(1024);
Future<Void> doReset = Future.future();
server.requestHandler(req -> {
doReset.setHandler(onSuccess(v -> {
req.response().reset(8);
}));
req.response().setChunked(true).write(Buffer.buffer(chunk));
});
startServer();
Context ctx = vertx.getOrCreateContext();
Handler<Throwable> resetHandler = err -> {
assertOnIOContext(ctx);
assertTrue(err instanceof StreamResetException);
StreamResetException reset = (StreamResetException) err;
assertEquals(8, reset.getCode());
complete();
};
ctx.runOnContext(v -> {
client.post(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
resp.exceptionHandler(resetHandler);
resp.handler(buff -> {
doReset.complete();
});
}).exceptionHandler(resetHandler).setChunked(true).write(chunk);
});
await();
}
@Test
public void testClientResetServerStreamDuringRequest() throws Exception {
Future<Void> bufReceived = Future.future();
server.requestHandler(req -> {
req.handler(buf -> {
bufReceived.complete();
});
req.exceptionHandler(err -> {
assertTrue(err instanceof StreamResetException);
});
req.response().exceptionHandler(err -> {
assertTrue(err instanceof StreamResetException);
assertEquals(10L, ((StreamResetException) err).getCode());
testComplete();
});
});
startServer();
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
fail();
}).setChunked(true).write(Buffer.buffer("hello"));
bufReceived.setHandler(ar -> {
req.reset(10);
});
await();
}
@Test
public void testClientResetServerStreamDuringResponse() throws Exception {
server.requestHandler(req -> {
req.exceptionHandler(err -> {
assertTrue(err instanceof StreamResetException);
});
req.response().exceptionHandler(err -> {
assertTrue(err instanceof StreamResetException);
assertEquals(10L, ((StreamResetException) err).getCode());
testComplete();
});
req.response().setChunked(true).write(Buffer.buffer("some-data"));
});
startServer();
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath");
req.handler(resp -> {
resp.exceptionHandler(this::fail);
req.reset(10);
assertIllegalStateException(() -> req.write(Buffer.buffer()));
assertIllegalStateException(req::end);
}).end(Buffer.buffer("hello"));
await();
}
@Test
public void testPushPromise() throws Exception {
waitFor(2);
server.requestHandler(req -> {
req.response().push(HttpMethod.GET, "/wibble?a=b", ar -> {
assertTrue(ar.succeeded());
HttpServerResponse response = ar.result();
response.end("the_content");
}).end();
});
startServer();
AtomicReference<Context> ctx = new AtomicReference<>();
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
Context current = Vertx.currentContext();
if (ctx.get() == null) {
ctx.set(current);
} else {
assertEquals(ctx.get(), current);
}
resp.endHandler(v -> {
complete();
});
});
req.pushHandler(pushedReq -> {
Context current = Vertx.currentContext();
if (ctx.get() == null) {
ctx.set(current);
} else {
assertEquals(ctx.get(), current);
}
assertOnIOContext(current);
assertEquals(HttpMethod.GET, pushedReq.method());
assertEquals("/wibble?a=b", pushedReq.uri());
assertEquals("/wibble", pushedReq.path());
assertEquals("a=b", pushedReq.query());
pushedReq.handler(resp -> {
assertEquals(200, resp.statusCode());
Buffer content = Buffer.buffer();
resp.handler(content::appendBuffer);
resp.endHandler(v -> {
complete();
});
});
});
req.end();
await();
}
@Test
public void testResetActivePushPromise() throws Exception {
server.requestHandler(req -> {
req.response().push(HttpMethod.GET, "/wibble", ar -> {
assertTrue(ar.succeeded());
HttpServerResponse response = ar.result();
response.exceptionHandler(err -> {
if (err instanceof StreamResetException) {
assertEquals(Http2Error.CANCEL.code(), ((StreamResetException) err).getCode());
testComplete();
}
});
response.setChunked(true).write("some_content");
});
});
startServer();
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
fail();
});
req.pushHandler(pushedReq -> {
pushedReq.handler(pushedResp -> {
pushedResp.handler(buff -> {
pushedReq.reset(Http2Error.CANCEL.code());
});
});
});
req.end();
await();
}
@Test
public void testResetPendingPushPromise() throws Exception {
server.requestHandler(req -> {
req.response().push(HttpMethod.GET, "/wibble", ar -> {
assertFalse(ar.succeeded());
testComplete();
});
});
startServer();
client.close();
client = vertx.createHttpClient(clientOptions.setInitialSettings(new io.vertx.core.http.Http2Settings().setMaxConcurrentStreams(0L)));
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
fail();
});
req.pushHandler(pushedReq -> {
pushedReq.reset(Http2Error.CANCEL.code());
});
req.end();
await();
}
@Test
public void testResetPushPromiseNoHandler() throws Exception {
server.requestHandler(req -> {
req.response().push(HttpMethod.GET, "/wibble", ar -> {
assertTrue(ar.succeeded());
HttpServerResponse resp = ar.result();
resp.setChunked(true).write("content");
resp.exceptionHandler(err -> {
assertTrue(err instanceof StreamResetException);
assertEquals(Http2Error.CANCEL.code(), ((StreamResetException) err).getCode());
testComplete();
});
});
});
startServer();
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
});
req.end();
await();
}
@Test
public void testConnectionHandler() throws Exception {
waitFor(2);
server.requestHandler(req -> {
req.response().end();
});
startServer();
AtomicReference<HttpConnection> connection = new AtomicReference<>();
HttpClientRequest req1 = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath");
req1.connectionHandler(conn -> {
Context ctx = Vertx.currentContext();
assertOnIOContext(ctx);
assertTrue(connection.compareAndSet(null, conn));
});
req1.handler(resp -> {
assertSame(connection.get(), req1.connection());
complete();
});
HttpClientRequest req2 = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath");
req2.connectionHandler(conn -> {
fail();
});
req2.handler(resp -> {
assertSame(connection.get(), req2.connection());
complete();
});
req1.end();
req2.end();
await();
}
@Test
public void testConnectionShutdownInConnectionHandler() throws Exception {
AtomicInteger serverStatus = new AtomicInteger();
server.connectionHandler(conn -> {
if (serverStatus.getAndIncrement() == 0) {
conn.goAwayHandler(ga -> {
assertEquals(0, ga.getErrorCode());
assertEquals(1, serverStatus.getAndIncrement());
});
conn.shutdownHandler(v -> {
assertEquals(2, serverStatus.getAndIncrement());
});
conn.closeHandler(v -> {
assertEquals(3, serverStatus.getAndIncrement());
});
}
});
server.requestHandler(req -> {
assertEquals(5, serverStatus.getAndIncrement());
req.response().end();
});
startServer();
AtomicInteger clientStatus = new AtomicInteger();
HttpClientRequest req1 = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath");
req1.connectionHandler(conn -> {
Context ctx = Vertx.currentContext();
conn.shutdownHandler(v -> {
assertOnIOContext(ctx);
clientStatus.compareAndSet(1, 2);
});
if (clientStatus.getAndIncrement() == 0) {
conn.shutdown();
}
});
req1.exceptionHandler(err -> {
fail();
});
req1.handler(resp -> {
assertEquals(2, clientStatus.getAndIncrement());
resp.endHandler(v -> {
testComplete();
});
});
req1.end();
await();
}
@Test
public void testServerShutdownConnection() throws Exception {
waitFor(2);
server.connectionHandler(HttpConnection::shutdown);
server.requestHandler(req -> fail());
startServer();
HttpClientRequest req1 = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath");
req1.connectionHandler(conn -> {
Context ctx = Vertx.currentContext();
conn.goAwayHandler(ga -> {
assertOnIOContext(ctx);
complete();
});
});
AtomicInteger count = new AtomicInteger();
req1.exceptionHandler(err -> {
if (count.getAndIncrement() == 0) {
complete();
}
});
req1.handler(resp -> {
fail();
});
req1.end();
await();
}
@Test
public void testReceivingGoAwayDiscardsTheConnection() throws Exception {
AtomicInteger reqCount = new AtomicInteger();
server.requestHandler(req -> {
switch (reqCount.getAndIncrement()) {
case 0:
req.connection().goAway(0);
break;
case 1:
req.response().end();
break;
default:
fail();
}
});
startServer();
HttpClientRequest req1 = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath");
req1.connectionHandler(conn -> {
AtomicInteger gaCount = new AtomicInteger();
conn.goAwayHandler(ga -> {
if (gaCount.getAndIncrement() == 0) {
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp2 -> {
testComplete();
}).setTimeout(5000).exceptionHandler(this::fail).end();
}
});
});
req1.handler(resp1 -> {
fail();
}).end();
await();
}
@Test
public void testSendingGoAwayDiscardsTheConnection() throws Exception {
AtomicInteger reqCount = new AtomicInteger();
server.requestHandler(req -> {
switch (reqCount.getAndIncrement()) {
case 0:
req.response().setChunked(true).write("some-data");
break;
case 1:
req.response().end();
break;
default:
fail();
}
});
startServer();
HttpClientRequest req1 = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath");
req1.handler(resp1 -> {
req1.connection().goAway(0);
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp2 -> {
testComplete();
}).setTimeout(5000).exceptionHandler(this::fail).end();
}).end();
await();
}
private Http2ConnectionHandler createHttpConnectionHandler(BiFunction<Http2ConnectionDecoder, Http2ConnectionEncoder, Http2FrameListener> handler) {
class Handler extends Http2ConnectionHandler {
public Handler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) {
super(decoder, encoder, initialSettings);
decoder.frameListener(handler.apply(decoder, encoder));
}
}
class Builder extends AbstractHttp2ConnectionHandlerBuilder<Handler, Builder> {
@Override
protected Handler build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) throws Exception {
return new Handler(decoder, encoder, initialSettings);
}
@Override
public Handler build() {
return super.build();
}
}
Builder builder = new Builder();
return builder.build();
}
private ServerBootstrap createH2Server(BiFunction<Http2ConnectionDecoder, Http2ConnectionEncoder, Http2FrameListener> handler) {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.channel(NioServerSocketChannel.class);
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
eventLoopGroups.add(eventLoopGroup);
bootstrap.group(eventLoopGroup);
bootstrap.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
SSLHelper sslHelper = new SSLHelper(serverOptions, Cert.SERVER_JKS.get(), null);
SslHandler sslHandler = new SslHandler(sslHelper.setApplicationProtocols(Arrays.asList(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)).createEngine((VertxInternal) vertx, DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT));
ch.pipeline().addLast(sslHandler);
ch.pipeline().addLast(new ApplicationProtocolNegotiationHandler("whatever") {
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
ChannelPipeline p = ctx.pipeline();
Http2ConnectionHandler clientHandler = createHttpConnectionHandler(handler);
p.addLast("handler", clientHandler);
return;
}
ctx.close();
throw new IllegalStateException("unknown protocol: " + protocol);
}
});
}
});
return bootstrap;
}
private ServerBootstrap createH2CServer(BiFunction<Http2ConnectionDecoder, Http2ConnectionEncoder, Http2FrameListener> handler, boolean upgrade) {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.group(new NioEventLoopGroup());
bootstrap.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
if (upgrade) {
HttpServerCodec sourceCodec = new HttpServerCodec();
HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> {
if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) {
return new Http2ServerUpgradeCodec(createHttpConnectionHandler(handler));
} else {
return null;
}
};
ch.pipeline().addLast(sourceCodec);
ch.pipeline().addLast(new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory));
} else {
Http2ConnectionHandler clientHandler = createHttpConnectionHandler(handler);
ch.pipeline().addLast("handler", clientHandler);
}
}
});
return bootstrap;
}
@Test
public void testStreamError() throws Exception {
waitFor(3);
ServerBootstrap bootstrap = createH2Server((dec, enc) -> new Http2EventAdapter() {
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
enc.writeHeaders(ctx, streamId, new DefaultHttp2Headers().status("200"), 0, false, ctx.newPromise());
// Send a corrupted frame on purpose to check we get the corresponding error in the request exception handler
// the error is : greater padding value 0c -> 1F
// ChannelFuture a = encoder.frameWriter().writeData(request.context, id, Buffer.buffer("hello").getByteBuf(), 12, false, request.context.newPromise());
// normal frame : 00 00 12 00 08 00 00 00 03 0c 68 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00 00
// corrupted frame : 00 00 12 00 08 00 00 00 03 1F 68 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00 00
ctx.channel().write(Buffer.buffer(new byte[]{
0x00, 0x00, 0x12, 0x00, 0x08, 0x00, 0x00, 0x00, (byte)(streamId & 0xFF), 0x1F, 0x68, 0x65, 0x6c, 0x6c,
0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
}).getByteBuf());
ctx.flush();
}
});
ChannelFuture s = bootstrap.bind(DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT).sync();
try {
Context ctx = vertx.getOrCreateContext();
ctx.runOnContext(v -> {
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
resp.exceptionHandler(err -> {
assertOnIOContext(ctx);
if (err instanceof Http2Exception) {
complete();
}
});
}).connectionHandler(conn -> {
conn.exceptionHandler(err -> {
assertOnIOContext(ctx);
if (err instanceof Http2Exception) {
complete();
}
});
}).exceptionHandler(err -> {
assertOnIOContext(ctx);
if (err instanceof Http2Exception) {
complete();
}
}).sendHead();
});
await();
} finally {
s.channel().close().sync();
}
}
@Test
public void testConnectionDecodeError() throws Exception {
waitFor(3);
ServerBootstrap bootstrap = createH2Server((dec, enc) -> new Http2EventAdapter() {
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
enc.writeHeaders(ctx, streamId, new DefaultHttp2Headers().status("200"), 0, false, ctx.newPromise());
enc.frameWriter().writeRstStream(ctx, 10, 0, ctx.newPromise());
ctx.flush();
}
});
ChannelFuture s = bootstrap.bind(DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT).sync();
try {
Context ctx = vertx.getOrCreateContext();
ctx.runOnContext(v -> {
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
resp.exceptionHandler(err -> {
assertOnIOContext(ctx);
if (err instanceof Http2Exception) {
complete();
}
});
}).connectionHandler(conn -> {
conn.exceptionHandler(err -> {
assertSame(ctx, Vertx.currentContext());
if (err instanceof Http2Exception) {
complete();
}
});
}).exceptionHandler(err -> {
assertOnIOContext(ctx);
if (err instanceof Http2Exception) {
complete();
}
}).sendHead();
});
await();
} finally {
s.channel().close().sync();
}
}
@Test
public void testInvalidServerResponse() throws Exception {
ServerBootstrap bootstrap = createH2Server((dec, enc) -> new Http2EventAdapter() {
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
enc.writeHeaders(ctx, streamId, new DefaultHttp2Headers().status("xyz"), 0, false, ctx.newPromise());
ctx.flush();
}
});
ChannelFuture s = bootstrap.bind(DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT).sync();
try {
Context ctx = vertx.getOrCreateContext();
ctx.runOnContext(v -> {
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
fail();
}).connectionHandler(conn -> {
conn.exceptionHandler(err -> {
fail();
});
}).exceptionHandler(err -> {
assertOnIOContext(ctx);
if (err instanceof NumberFormatException) {
testComplete();
}
}).end();
});
await();
} finally {
s.channel().close().sync();
}
}
@Test
public void testResponseCompressionEnabled() throws Exception {
testResponseCompression(true);
}
@Test
public void testResponseCompressionDisabled() throws Exception {
testResponseCompression(false);
}
private void testResponseCompression(boolean enabled) throws Exception {
byte[] expected = TestUtils.randomAlphaString(1000).getBytes();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream in = new GZIPOutputStream(baos);
in.write(expected);
in.close();
byte[] compressed = baos.toByteArray();
server.close();
server = vertx.createHttpServer(serverOptions);
server.requestHandler(req -> {
assertEquals(enabled ? "deflate, gzip" : null, req.getHeader(HttpHeaderNames.ACCEPT_ENCODING));
req.response().putHeader(HttpHeaderNames.CONTENT_ENCODING.toLowerCase(), "gzip").end(Buffer.buffer(compressed));
});
startServer();
client.close();
client = vertx.createHttpClient(clientOptions.setTryUseCompression(enabled));
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
String encoding = resp.getHeader(HttpHeaderNames.CONTENT_ENCODING);
assertEquals(enabled ? null : "gzip", encoding);
resp.bodyHandler(buff -> {
assertEquals(Buffer.buffer(enabled ? expected : compressed), buff);
testComplete();
});
}).end();
await();
}
@Test
public void test100Continue() throws Exception {
AtomicInteger status = new AtomicInteger();
server.close();
server = vertx.createHttpServer(serverOptions.setHandle100ContinueAutomatically(true));
server.requestHandler(req -> {
status.getAndIncrement();
HttpServerResponse resp = req.response();
req.bodyHandler(body -> {
assertEquals(2, status.getAndIncrement());
assertEquals("request-body", body.toString());
resp.putHeader("wibble", "wibble-value").end("response-body");
});
});
startServer();
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
assertEquals(3, status.getAndIncrement());
resp.bodyHandler(body -> {
assertEquals(4, status.getAndIncrement());
assertEquals("response-body", body.toString());
testComplete();
});
});
req.putHeader("expect", "100-continue");
req.continueHandler(v -> {
Context ctx = Vertx.currentContext();
assertOnIOContext(ctx);
status.getAndIncrement();
req.end(Buffer.buffer("request-body"));
});
req.sendHead(version -> {
assertEquals(3, req.streamId());
});
await();
}
@Test
public void testNetSocketConnect() throws Exception {
waitFor(2);
server.requestHandler(req -> {
NetSocket socket = req.netSocket();
AtomicInteger status = new AtomicInteger();
socket.handler(buff -> {
switch (status.getAndIncrement()) {
case 0:
assertEquals(Buffer.buffer("some-data"), buff);
socket.write(buff);
break;
case 1:
assertEquals(Buffer.buffer("last-data"), buff);
break;
default:
fail();
break;
}
});
socket.endHandler(v -> {
assertEquals(2, status.getAndIncrement());
socket.write(Buffer.buffer("last-data"));
});
socket.closeHandler(v -> {
assertEquals(3, status.getAndIncrement());
complete();
});
});
startServer();
client.request(HttpMethod.CONNECT, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
assertEquals(200, resp.statusCode());
NetSocket socket = resp.netSocket();
StringBuilder received = new StringBuilder();
AtomicInteger count = new AtomicInteger();
socket.handler(buff -> {
if (buff.length() > 0) {
received.append(buff.toString());
if (received.toString().equals("some-data")) {
received.setLength(0);
socket.end(Buffer.buffer("last-data"));
} else if (received.toString().equals("last-data")) {
assertEquals(0, count.getAndIncrement());
}
}
});
socket.endHandler(v -> {
assertEquals(1, count.getAndIncrement());
});
socket.closeHandler(v -> {
assertEquals(2, count.getAndIncrement());
complete();
});
socket.write(Buffer.buffer("some-data"));
}).sendHead();
await();
}
@Test
public void testServerCloseNetSocket() throws Exception {
waitFor(2);
AtomicInteger status = new AtomicInteger();
server.requestHandler(req -> {
NetSocket socket = req.netSocket();
socket.handler(buff -> {
switch (status.getAndIncrement()) {
case 0:
assertEquals(Buffer.buffer("some-data"), buff);
socket.end(buff);
break;
case 1:
assertEquals(Buffer.buffer("last-data"), buff);
break;
default:
fail();
break;
}
});
socket.endHandler(v -> {
assertEquals(2, status.getAndIncrement());
});
socket.closeHandler(v -> {
assertEquals(3, status.getAndIncrement());
complete();
});
});
startServer();
client.request(HttpMethod.CONNECT, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
assertEquals(200, resp.statusCode());
NetSocket socket = resp.netSocket();
AtomicInteger count = new AtomicInteger();
socket.handler(buff -> {
switch (count.getAndIncrement()) {
case 0:
assertEquals("some-data", buff.toString());
break;
default:
fail();
break;
}
});
socket.endHandler(v -> {
assertEquals(1, count.getAndIncrement());
socket.end(Buffer.buffer("last-data"));
});
socket.closeHandler(v -> {
assertEquals(2, count.getAndIncrement());
complete();
});
socket.write(Buffer.buffer("some-data"));
}).sendHead();
await();
}
@Test
public void testSendHeadersCompletionHandler() throws Exception {
AtomicInteger status = new AtomicInteger();
server.requestHandler(req -> {
req.response().end();
});
startServer();
HttpClientRequest req = client.request(HttpMethod.CONNECT, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
assertEquals(1, status.getAndIncrement());
resp.endHandler(v -> {
assertEquals(3, status.getAndIncrement());
testComplete();
});
});
req.endHandler(v -> {
assertEquals(2, status.getAndIncrement());
});
req.sendHead(version -> {
assertEquals(0, status.getAndIncrement());
assertSame(HttpVersion.HTTP_2, version);
req.end();
});
await();
}
@Test
public void testUnknownFrame() throws Exception {
Buffer expectedSend = TestUtils.randomBuffer(500);
Buffer expectedRecv = TestUtils.randomBuffer(500);
server.requestHandler(req -> {
req.customFrameHandler(frame -> {
assertEquals(10, frame.type());
assertEquals(253, frame.flags());
assertEquals(expectedSend, frame.payload());
req.response().writeCustomFrame(12, 134, expectedRecv).end();
});
});
startServer();
AtomicInteger status = new AtomicInteger();
HttpClientRequest req = client.request(HttpMethod.GET, DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
Context ctx = Vertx.currentContext();
assertEquals(0, status.getAndIncrement());
resp.customFrameHandler(frame -> {
assertOnIOContext(ctx);
assertEquals(1, status.getAndIncrement());
assertEquals(12, frame.type());
assertEquals(134, frame.flags());
assertEquals(expectedRecv, frame.payload());
});
resp.endHandler(v -> {
assertEquals(2, status.getAndIncrement());
testComplete();
});
});
req.sendHead(version -> {
assertSame(HttpVersion.HTTP_2, version);
req.writeCustomFrame(10, 253, expectedSend);
req.end();
});
await();
}
@Test
public void testClearTextUpgrade() throws Exception {
testClearText(true);
}
@Test
public void testClearTextWithPriorKnowledge() throws Exception {
testClearText(false);
}
private void testClearText(boolean upgrade) throws Exception {
ServerBootstrap bootstrap = createH2CServer((dec, enc) -> new Http2EventAdapter() {
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
enc.writeHeaders(ctx, streamId, new DefaultHttp2Headers().status("200"), 0, true, ctx.newPromise());
ctx.flush();
}
}, upgrade);
ChannelFuture s = bootstrap.bind(DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT).sync();
try {
client.close();
client = vertx.createHttpClient(clientOptions.setUseAlpn(false).setSsl(false).setHttp2ClearTextUpgrade(upgrade));
client.get(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath", resp -> {
assertEquals(HttpVersion.HTTP_2, resp.version());
testComplete();
}).exceptionHandler(this::fail).end();
await();
} finally {
s.channel().close().sync();
}
}
@Test
public void testRejectClearTextUpgrade() throws Exception {
System.setProperty("vertx.disableH2c", "true");
try {
server.close();
server = vertx.createHttpServer(serverOptions.setUseAlpn(false).setSsl(false).setHost(DEFAULT_HTTP_HOST).setPort(DEFAULT_HTTP_PORT));
server.requestHandler(req -> {
assertEquals(HttpVersion.HTTP_1_1, req.version());
req.response().end();
});
startServer();
client.close();
client = vertx.createHttpClient(clientOptions.setUseAlpn(false).setSsl(false));
client.get(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath", resp -> {
assertEquals(HttpVersion.HTTP_1_1, resp.version());
testComplete();
}).exceptionHandler(this::fail).end();
await();
} finally {
System.clearProperty("vertx.disableH2c");
}
}
@Test
public void testIdleTimeout() throws Exception {
testIdleTimeout(serverOptions, clientOptions.setDefaultPort(DEFAULT_HTTPS_PORT));
}
@Test
public void testIdleTimeoutClearText() throws Exception {
testIdleTimeout(new HttpServerOptions().setPort(DEFAULT_HTTP_PORT).setHost(DEFAULT_HTTPS_HOST),
clientOptions.setDefaultPort(DEFAULT_HTTP_PORT).setUseAlpn(false).setSsl(false).setHttp2ClearTextUpgrade(true));
}
@Test
public void testIdleTimeoutClearTextDirect() throws Exception {
testIdleTimeout(new HttpServerOptions().setPort(DEFAULT_HTTP_PORT).setHost(DEFAULT_HTTPS_HOST),
clientOptions.setDefaultPort(DEFAULT_HTTP_PORT).setUseAlpn(false).setSsl(false).setHttp2ClearTextUpgrade(false));
}
private void testIdleTimeout(HttpServerOptions serverOptions, HttpClientOptions clientOptions) throws Exception {
waitFor(4);
server.close();
server = vertx.createHttpServer(serverOptions);
server.requestHandler(req -> {
req.connection().closeHandler(v -> {
complete();
});
req.response().setChunked(true).write("somedata");
});
startServer();
client.close();
client = vertx.createHttpClient(clientOptions.setIdleTimeout(2));
Context ctx = vertx.getOrCreateContext();
ctx.runOnContext(v1 -> {
HttpClientRequest req = client.get("/somepath", resp -> {
resp.exceptionHandler(err -> {
assertSame(ctx, Vertx.currentContext());
assertOnIOContext(ctx);
complete();
});
});
req.exceptionHandler(err -> {
complete();
});
req.connectionHandler(conn -> {
conn.closeHandler(v2 -> {
assertSame(ctx, Vertx.currentContext());
complete();
});
});
req.sendHead();
});
await();
}
@Test
public void testIdleTimoutNoConnections() throws Exception {
waitFor(4);
AtomicLong time = new AtomicLong();
server.requestHandler(req -> {
req.connection().closeHandler(v -> {
complete();
});
req.response().end("somedata");
complete();
});
startServer();
client.close();
client = vertx.createHttpClient(clientOptions.setIdleTimeout(2));
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
resp.exceptionHandler(err -> {
fail();
});
resp.endHandler(v -> {
time.set(System.currentTimeMillis());
complete();
});
});
req.exceptionHandler(err -> {
fail();
});
req.connectionHandler(conn -> {
conn.closeHandler(v -> {
assertTrue(System.currentTimeMillis() - time.get() > 1000);
complete();
});
});
req.end();
await();
}
@Test
public void testSendPing() throws Exception {
waitFor(2);
Buffer expected = TestUtils.randomBuffer(8);
Context ctx = vertx.getOrCreateContext();
server.close();
server.connectionHandler(conn -> {
conn.pingHandler(data -> {
assertEquals(expected, data);
complete();
});
});
server.requestHandler(req -> {});
startServer(ctx);
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
});
req.connectionHandler(conn -> {
conn.ping(expected, ar -> {
assertTrue(ar.succeeded());
Buffer buff = ar.result();
assertEquals(expected, buff);
complete();
});
});
req.end();
await();
}
@Test
public void testReceivePing() throws Exception {
Buffer expected = TestUtils.randomBuffer(8);
Context ctx = vertx.getOrCreateContext();
server.close();
server.connectionHandler(conn -> {
conn.ping(expected, ar -> {
});
});
server.requestHandler(req -> {});
startServer(ctx);
HttpClientRequest req = client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
});
req.connectionHandler(conn -> {
conn.pingHandler(data -> {
assertEquals(expected, data);
complete();
});
});
req.end();
await();
}
@Test
public void testMaxConcurrencySingleConnection() throws Exception {
testMaxConcurrency(1, 5);
}
@Test
public void testMaxConcurrencyMultipleConnections() throws Exception {
testMaxConcurrency(3, 5);
}
private void testMaxConcurrency(int poolSize, int maxConcurrency) throws Exception {
int rounds = 1 + poolSize;
int maxRequests = poolSize * maxConcurrency;
int totalRequests = maxRequests + maxConcurrency;
Set<HttpConnection> serverConns = new HashSet<>();
server.connectionHandler(conn -> {
serverConns.add(conn);
assertTrue(serverConns.size() <= poolSize);
});
ArrayList<HttpServerRequest> requests = new ArrayList<>();
server.requestHandler(req -> {
if (requests.size() < maxRequests) {
requests.add(req);
if (requests.size() == maxRequests) {
vertx.setTimer(300, v -> {
assertEquals(maxRequests, requests.size());
requests.forEach(r -> r.response().end());
});
}
} else {
req.response().end();
}
});
startServer();
client.close();
client = vertx.createHttpClient(new HttpClientOptions(clientOptions).
setHttp2MaxPoolSize(poolSize).
setHttp2MultiplexingLimit(maxConcurrency));
AtomicInteger respCount = new AtomicInteger();
Set<HttpConnection> clientConnections = Collections.synchronizedSet(new HashSet<>());
for (int j = 0;j < rounds;j++) {
for (int i = 0;i < maxConcurrency;i++) {
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
resp.endHandler(v -> {
if (respCount.incrementAndGet() == totalRequests) {
testComplete();
}
});
}).connectionHandler(clientConnections::add).end();
}
if (j < poolSize) {
int threshold = j + 1;
assertWaitUntil(() -> clientConnections.size() == threshold);
}
}
await();
}
@Test
public void testConnectionWindowSize() throws Exception {
ServerBootstrap bootstrap = createH2Server((decoder, encoder) -> new Http2EventAdapter() {
@Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) throws Http2Exception {
vertx.runOnContext(v -> {
assertEquals(65535, windowSizeIncrement);
testComplete();
});
}
});
ChannelFuture s = bootstrap.bind(DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT).sync();
client.close();
client = vertx.createHttpClient(new HttpClientOptions(clientOptions).setHttp2ConnectionWindowSize(65535 * 2));
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
}).exceptionHandler(this::fail).end();
await();
}
@Test
public void testUpdateConnectionWindowSize() throws Exception {
ServerBootstrap bootstrap = createH2Server((decoder, encoder) -> new Http2EventAdapter() {
@Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) throws Http2Exception {
vertx.runOnContext(v -> {
assertEquals(65535, windowSizeIncrement);
testComplete();
});
}
});
ChannelFuture s = bootstrap.bind(DEFAULT_HTTPS_HOST, DEFAULT_HTTPS_PORT).sync();
client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
}).connectionHandler(conn -> {
assertEquals(65535, conn.getWindowSize());
conn.setWindowSize(65535 + 10000);
assertEquals(65535 + 10000, conn.getWindowSize());
conn.setWindowSize(65535 + 65535);
assertEquals(65535 + 65535, conn.getWindowSize());
}).end();
await();
}
/*
@Test
public void testFillsSingleConnection() throws Exception {
Set<HttpConnection> serverConns = new HashSet<>();
List<HttpServerRequest> requests = new ArrayList<>();
server.requestHandler(req -> {
requests.add(req);
serverConns.add(req.connection());
if (requests.size() == 10) {
System.out.println("requestsPerConn = " + serverConns);
}
});
startServer();
client.close();
client = vertx.createHttpClient(new HttpClientOptions(clientOptions).
setHttp2MaxPoolSize(2).
setHttp2MaxStreams(10));
AtomicInteger respCount = new AtomicInteger();
for (int i = 0;i < 10;i++) {
client.getNow(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> {
resp.endHandler(v -> {
});
});
}
await();
}
*/
@Test
public void testWorkerVerticleException() throws Exception {
Verticle workerVerticle = new AbstractVerticle() {
@Override
public void start() throws Exception {
try {
vertx.createHttpClient(createHttp2ClientOptions());
fail("HttpClient should not work with HTTP_2");
} catch(Exception ex) {
assertEquals("Cannot use HttpClient with HTTP_2 in a worker", ex.getMessage());
complete();
}
}
};
vertx.deployVerticle(workerVerticle, new DeploymentOptions().setWorker(true));
await();
}
@Test
public void testExecuteBlockingException() throws Exception {
vertx.executeBlocking(fut -> {
try {
vertx.createHttpClient(createHttp2ClientOptions());
fail("HttpClient should not work with HTTP_2 inside executeBlocking");
} catch(Exception ex) {
assertEquals("Cannot use HttpClient with HTTP_2 in a worker", ex.getMessage());
complete();
}
}, null);
await();
}
}