Java tutorial
/* * 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.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; 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.NioSocketChannel; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http2.AbstractHttp2ConnectionHandlerBuilder; import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.DefaultHttp2Headers; import io.netty.handler.codec.http2.Http2Connection; 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.Http2Flags; import io.netty.handler.codec.http2.Http2FrameAdapter; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Stream; import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.SslHandler; import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.MultiMap; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpClient; 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.HttpServer; 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.http.impl.HttpUtils; import io.vertx.core.impl.VertxInternal; import io.vertx.core.net.NetSocket; import io.vertx.core.net.impl.SSLHelper; import io.vertx.core.streams.ReadStream; import io.vertx.core.streams.WriteStream; import io.vertx.test.core.tls.Trust; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.ClosedChannelException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; 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.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import static io.vertx.test.core.TestUtils.assertIllegalStateException; /** * @author <a href="mailto:julien@julienviet.com">Julien Viet</a> */ public class Http2ServerTest extends Http2TestBase { private static Http2Headers headers(String method, String scheme, String path) { return new DefaultHttp2Headers().method(method).scheme(scheme).path(path); } private static Http2Headers GET(String scheme, String path) { return headers("GET", scheme, path); } private static Http2Headers GET(String path) { return headers("GET", "https", path); } private static Http2Headers POST(String path) { return headers("POST", "https", path); } class TestClient { final Http2Settings settings = new Http2Settings(); public class Connection { public final Channel channel; public final ChannelHandlerContext context; public final Http2Connection connection; public final Http2ConnectionEncoder encoder; public final Http2ConnectionDecoder decoder; public Connection(ChannelHandlerContext context, Http2Connection connection, Http2ConnectionEncoder encoder, Http2ConnectionDecoder decoder) { this.channel = context.channel(); this.context = context; this.connection = connection; this.encoder = encoder; this.decoder = decoder; } public int nextStreamId() { return connection.local().incrementAndGetNextStreamId(); } } class TestClientHandler extends Http2ConnectionHandler { private final Consumer<Connection> requestHandler; private boolean handled; public TestClientHandler(Consumer<Connection> requestHandler, Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) { super(decoder, encoder, initialSettings); this.requestHandler = requestHandler; } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { super.handlerAdded(ctx); if (ctx.channel().isActive()) { checkHandle(ctx); } } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); checkHandle(ctx); } private void checkHandle(ChannelHandlerContext ctx) { if (!handled) { handled = true; Connection conn = new Connection(ctx, connection(), encoder(), decoder()); requestHandler.accept(conn); } } } class TestClientHandlerBuilder extends AbstractHttp2ConnectionHandlerBuilder<TestClientHandler, TestClientHandlerBuilder> { private final Consumer<Connection> requestHandler; public TestClientHandlerBuilder(Consumer<Connection> requestHandler) { this.requestHandler = requestHandler; } @Override protected TestClientHandler build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) throws Exception { return new TestClientHandler(requestHandler, decoder, encoder, initialSettings); } public TestClientHandler build(Http2Connection conn) { connection(conn); initialSettings(settings); frameListener(new Http2EventAdapter() { @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { return super.onDataRead(ctx, streamId, data, padding, endOfStream); } }); return super.build(); } } protected ChannelInitializer channelInitializer(int port, String host, Consumer<Connection> handler) { return new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel ch) throws Exception { SSLHelper sslHelper = new SSLHelper(new HttpClientOptions().setUseAlpn(true).setSsl(true), null, Trust.SERVER_JKS.get()); SslHandler sslHandler = sslHelper .setApplicationProtocols(Arrays.asList(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)) .createSslHandler((VertxInternal) vertx, host, 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(); Http2Connection connection = new DefaultHttp2Connection(false); TestClientHandlerBuilder clientHandlerBuilder = new TestClientHandlerBuilder( handler); TestClientHandler clientHandler = clientHandlerBuilder.build(connection); p.addLast(clientHandler); return; } ctx.close(); throw new IllegalStateException("unknown protocol: " + protocol); } }); } }; } public ChannelFuture connect(int port, String host, Consumer<Connection> handler) { Bootstrap bootstrap = new Bootstrap(); NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup(); eventLoopGroups.add(eventLoopGroup); bootstrap.channel(NioSocketChannel.class); bootstrap.group(eventLoopGroup); bootstrap.handler(channelInitializer(port, host, handler)); return bootstrap.connect(new InetSocketAddress(host, port)); } } private List<EventLoopGroup> eventLoopGroups = new ArrayList<>(); @Override public void setUp() throws Exception { eventLoopGroups.clear(); super.setUp(); } @Override protected void tearDown() throws Exception { super.tearDown(); for (EventLoopGroup eventLoopGroup : eventLoopGroups) { eventLoopGroup.shutdownGracefully(0, 10, TimeUnit.SECONDS); } } @Test public void testConnectionHandler() throws Exception { waitFor(2); Context ctx = vertx.getOrCreateContext(); server.close(); server.connectionHandler(conn -> { assertOnIOContext(ctx); complete(); }); server.requestHandler(req -> fail()); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { vertx.runOnContext(v -> { complete(); }); }); fut.sync(); await(); } @Test public void testServerInitialSettings() throws Exception { io.vertx.core.http.Http2Settings settings = TestUtils.randomHttp2Settings(); server.close(); server = vertx.createHttpServer(serverOptions.setInitialSettings(settings)); server.requestHandler(req -> fail()); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(new Http2FrameAdapter() { @Override public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings newSettings) throws Http2Exception { vertx.runOnContext(v -> { assertEquals((Long) settings.getHeaderTableSize(), newSettings.headerTableSize()); assertEquals((Long) settings.getMaxConcurrentStreams(), newSettings.maxConcurrentStreams()); assertEquals((Integer) settings.getInitialWindowSize(), newSettings.initialWindowSize()); assertEquals((Integer) settings.getMaxFrameSize(), newSettings.maxFrameSize()); assertEquals((Long) settings.getMaxHeaderListSize(), newSettings.maxHeaderListSize()); assertEquals(settings.get('\u0007'), newSettings.get('\u0007')); testComplete(); }); } }); }); fut.sync(); 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, ar -> { assertSame(otherContext, Vertx.currentContext()); io.vertx.core.http.Http2Settings ackedSettings = conn.settings(); assertEquals(expectedSettings.getMaxHeaderListSize(), ackedSettings.getMaxHeaderListSize()); assertEquals(expectedSettings.getMaxFrameSize(), ackedSettings.getMaxFrameSize()); assertEquals(expectedSettings.getInitialWindowSize(), ackedSettings.getInitialWindowSize()); assertEquals(expectedSettings.getMaxConcurrentStreams(), ackedSettings.getMaxConcurrentStreams()); assertEquals(expectedSettings.getHeaderTableSize(), ackedSettings.getHeaderTableSize()); assertEquals(expectedSettings.get('\u0007'), ackedSettings.get(7)); complete(); }); }); }); server.requestHandler(req -> { fail(); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(new Http2FrameAdapter() { AtomicInteger count = new AtomicInteger(); @Override public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings newSettings) throws Http2Exception { vertx.runOnContext(v -> { switch (count.getAndIncrement()) { case 0: // Initial settings break; case 1: // Server sent settings assertEquals((Long) expectedSettings.getMaxHeaderListSize(), newSettings.maxHeaderListSize()); assertEquals((Integer) expectedSettings.getMaxFrameSize(), newSettings.maxFrameSize()); assertEquals((Integer) expectedSettings.getInitialWindowSize(), newSettings.initialWindowSize()); assertEquals((Long) expectedSettings.getMaxConcurrentStreams(), newSettings.maxConcurrentStreams()); assertEquals(null, newSettings.headerTableSize()); complete(); break; default: fail(); } }); } }); }); fut.sync(); await(); } @Test public void testClientSettings() throws Exception { Context ctx = vertx.getOrCreateContext(); io.vertx.core.http.Http2Settings initialSettings = TestUtils.randomHttp2Settings(); io.vertx.core.http.Http2Settings updatedSettings = TestUtils.randomHttp2Settings(); Future<Void> settingsRead = Future.future(); AtomicInteger count = new AtomicInteger(); server.connectionHandler(conn -> { io.vertx.core.http.Http2Settings settings = conn.remoteSettings(); assertEquals(true, settings.isPushEnabled()); // Netty bug ? // Nothing has been yet received so we should get Integer.MAX_VALUE // assertEquals(Integer.MAX_VALUE, settings.getMaxHeaderListSize()); assertEquals(io.vertx.core.http.Http2Settings.DEFAULT_MAX_FRAME_SIZE, settings.getMaxFrameSize()); assertEquals(io.vertx.core.http.Http2Settings.DEFAULT_INITIAL_WINDOW_SIZE, settings.getInitialWindowSize()); assertEquals((Long) (long) Integer.MAX_VALUE, (Long) (long) settings.getMaxConcurrentStreams()); assertEquals(io.vertx.core.http.Http2Settings.DEFAULT_HEADER_TABLE_SIZE, settings.getHeaderTableSize()); conn.remoteSettingsHandler(update -> { assertOnIOContext(ctx); switch (count.getAndIncrement()) { case 0: assertEquals(initialSettings.isPushEnabled(), update.isPushEnabled()); assertEquals(initialSettings.getMaxHeaderListSize(), update.getMaxHeaderListSize()); assertEquals(initialSettings.getMaxFrameSize(), update.getMaxFrameSize()); assertEquals(initialSettings.getInitialWindowSize(), update.getInitialWindowSize()); assertEquals(initialSettings.getMaxConcurrentStreams(), update.getMaxConcurrentStreams()); assertEquals(initialSettings.getHeaderTableSize(), update.getHeaderTableSize()); assertEquals(initialSettings.get('\u0007'), update.get(7)); settingsRead.complete(); break; case 1: assertEquals(updatedSettings.isPushEnabled(), update.isPushEnabled()); assertEquals(updatedSettings.getMaxHeaderListSize(), update.getMaxHeaderListSize()); assertEquals(updatedSettings.getMaxFrameSize(), update.getMaxFrameSize()); assertEquals(updatedSettings.getInitialWindowSize(), update.getInitialWindowSize()); assertEquals(updatedSettings.getMaxConcurrentStreams(), update.getMaxConcurrentStreams()); assertEquals(updatedSettings.getHeaderTableSize(), update.getHeaderTableSize()); assertEquals(updatedSettings.get('\u0007'), update.get(7)); testComplete(); break; } }); }); server.requestHandler(req -> { fail(); }); startServer(ctx); TestClient client = new TestClient(); client.settings.putAll(HttpUtils.fromVertxSettings(initialSettings)); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { settingsRead.setHandler(ar -> { request.encoder.writeSettings(request.context, HttpUtils.fromVertxSettings(updatedSettings), request.context.newPromise()); request.context.flush(); }); }); fut.sync(); await(); } @Test public void testGet() throws Exception { String expected = TestUtils.randomAlphaString(1000); AtomicBoolean requestEnded = new AtomicBoolean(); Context ctx = vertx.getOrCreateContext(); server.requestHandler(req -> { assertOnIOContext(ctx); req.endHandler(v -> { assertOnIOContext(ctx); requestEnded.set(true); }); HttpServerResponse resp = req.response(); assertEquals(HttpMethod.GET, req.method()); assertEquals(DEFAULT_HTTPS_HOST_AND_PORT, req.host()); assertEquals("/", req.path()); assertEquals(DEFAULT_HTTPS_HOST_AND_PORT, req.getHeader(":authority")); assertTrue(req.isSSL()); assertEquals("https", req.getHeader(":scheme")); assertEquals("/", req.getHeader(":path")); assertEquals("GET", req.getHeader(":method")); 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)); assertEquals(Collections.singletonList("cookie_1; cookie_2; cookie_3"), req.headers().getAll("cookie")); resp.putHeader("content-type", "text/plain"); resp.putHeader("Foo_response", "foo_response_value"); resp.putHeader("bar_response", "bar_response_value"); resp.putHeader("juu_response", (List<String>) Arrays.asList("juu_response_value_1", "juu_response_value_2")); resp.end(expected); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.decoder.frameListener(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 -> { assertEquals(id, streamId); assertEquals("200", headers.status().toString()); assertEquals("text/plain", headers.get("content-type").toString()); assertEquals("foo_response_value", headers.get("foo_response").toString()); assertEquals("bar_response_value", headers.get("bar_response").toString()); assertEquals(2, headers.getAll("juu_response").size()); assertEquals("juu_response_value_1", headers.getAll("juu_response").get(0).toString()); assertEquals("juu_response_value_2", headers.getAll("juu_response").get(1).toString()); assertFalse(endStream); }); } @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { String actual = data.toString(StandardCharsets.UTF_8); vertx.runOnContext(v -> { assertEquals(id, streamId); assertEquals(expected, actual); assertTrue(endOfStream); testComplete(); }); return super.onDataRead(ctx, streamId, data, padding, endOfStream); } }); Http2Headers headers = GET("/").authority(DEFAULT_HTTPS_HOST_AND_PORT); headers.set("foo_request", "foo_request_value"); headers.set("bar_request", "bar_request_value"); headers.set("juu_request", "juu_request_value_1", "juu_request_value_2"); headers.set("cookie", Arrays.asList("cookie_1", "cookie_2", "cookie_3")); request.encoder.writeHeaders(request.context, id, headers, 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testStatusMessage() throws Exception { server.requestHandler(req -> { HttpServerResponse resp = req.response(); resp.setStatusCode(404); assertEquals("Not Found", resp.getStatusMessage()); resp.setStatusMessage("whatever"); assertEquals("whatever", resp.getStatusMessage()); testComplete(); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testURI() throws Exception { server.requestHandler(req -> { assertEquals("/some/path", req.path()); assertEquals("foo=foo_value&bar=bar_value_1&bar=bar_value_2", req.query()); assertEquals("/some/path?foo=foo_value&bar=bar_value_1&bar=bar_value_2", req.uri()); assertEquals("http://whatever.com/some/path?foo=foo_value&bar=bar_value_1&bar=bar_value_2", req.absoluteURI()); assertEquals("/some/path?foo=foo_value&bar=bar_value_1&bar=bar_value_2", req.getHeader(":path")); assertEquals("whatever.com", req.host()); MultiMap params = req.params(); Set<String> names = params.names(); assertEquals(2, names.size()); assertTrue(names.contains("foo")); assertTrue(names.contains("bar")); assertEquals("foo_value", params.get("foo")); assertEquals(Collections.singletonList("foo_value"), params.getAll("foo")); assertEquals("bar_value_2", params.get("bar")); assertEquals(Arrays.asList("bar_value_1", "bar_value_2"), params.getAll("bar")); testComplete(); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2Headers headers = new DefaultHttp2Headers().method("GET").scheme("http").authority("whatever.com") .path("/some/path?foo=foo_value&bar=bar_value_1&bar=bar_value_2"); request.encoder.writeHeaders(request.context, id, headers, 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testHeadersEndHandler() throws Exception { Context ctx = vertx.getOrCreateContext(); server.requestHandler(req -> { HttpServerResponse resp = req.response(); resp.setChunked(true); resp.putHeader("some", "some-header"); resp.headersEndHandler(v -> { assertOnIOContext(ctx); assertFalse(resp.headWritten()); resp.putHeader("extra", "extra-header"); }); resp.write("something"); assertTrue(resp.headWritten()); resp.end(); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(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 -> { assertEquals("some-header", headers.get("some").toString()); assertEquals("extra-header", headers.get("extra").toString()); testComplete(); }); } }); int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testBodyEndHandler() throws Exception { server.requestHandler(req -> { HttpServerResponse resp = req.response(); resp.setChunked(true); AtomicInteger count = new AtomicInteger(); resp.bodyEndHandler(v -> { assertEquals(0, count.getAndIncrement()); assertTrue(resp.ended()); }); resp.write("something"); assertEquals(0, count.get()); resp.end(); assertEquals(1, count.get()); testComplete(); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testPost() throws Exception { Context ctx = vertx.getOrCreateContext(); Buffer expectedContent = TestUtils.randomBuffer(1000); Buffer postContent = Buffer.buffer(); server.requestHandler(req -> { assertOnIOContext(ctx); req.handler(buff -> { assertOnIOContext(ctx); postContent.appendBuffer(buff); }); req.endHandler(v -> { assertOnIOContext(ctx); req.response().putHeader("content-type", "text/plain").end(""); assertEquals(expectedContent, postContent); testComplete(); }); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, POST("/").set("content-type", "text/plain"), 0, false, request.context.newPromise()); request.encoder.writeData(request.context, id, expectedContent.getByteBuf(), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testPostFileUpload() throws Exception { Context ctx = vertx.getOrCreateContext(); server.requestHandler(req -> { Buffer tot = Buffer.buffer(); req.setExpectMultipart(true); req.uploadHandler(upload -> { assertOnIOContext(ctx); assertEquals("file", upload.name()); assertEquals("tmp-0.txt", upload.filename()); assertEquals("image/gif", upload.contentType()); upload.handler(tot::appendBuffer); upload.endHandler(v -> { assertEquals(tot, Buffer.buffer("some-content")); testComplete(); }); }); req.endHandler(v -> { assertEquals(0, req.formAttributes().size()); req.response().putHeader("content-type", "text/plain").end("done"); }); }); startServer(ctx); String contentType = "multipart/form-data; boundary=a4e41223-a527-49b6-ac1c-315d76be757e"; String contentLength = "225"; String body = "--a4e41223-a527-49b6-ac1c-315d76be757e\r\n" + "Content-Disposition: form-data; name=\"file\"; filename=\"tmp-0.txt\"\r\n" + "Content-Type: image/gif; charset=utf-8\r\n" + "Content-Length: 12\r\n" + "\r\n" + "some-content\r\n" + "--a4e41223-a527-49b6-ac1c-315d76be757e--\r\n"; TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, POST("/form").set("content-type", contentType).set("content-length", contentLength), 0, false, request.context.newPromise()); request.encoder.writeData(request.context, id, Buffer.buffer(body).getByteBuf(), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testInvalidPostFileUpload() throws Exception { server.requestHandler(req -> { req.setExpectMultipart(true); AtomicInteger errCount = new AtomicInteger(); req.exceptionHandler(err -> { errCount.incrementAndGet(); }); req.endHandler(v -> { assertTrue(errCount.get() > 0); testComplete(); }); }); startServer(); String contentType = "multipart/form-data; boundary=a4e41223-a527-49b6-ac1c-315d76be757e"; String contentLength = "225"; String body = "--a4e41223-a527-49b6-ac1c-315d76be757e\r\n" + "Content-Disposition: form-data; name=\"file\"; filename=\"tmp-0.txt\"\r\n" + "Content-Type: image/gif; charset=ABCD\r\n" + "Content-Length: 12\r\n" + "\r\n" + "some-content\r\n" + "--a4e41223-a527-49b6-ac1c-315d76be757e--\r\n"; TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, POST("/form").set("content-type", contentType).set("content-length", contentLength), 0, false, request.context.newPromise()); request.encoder.writeData(request.context, id, Buffer.buffer(body).getByteBuf(), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testConnect() throws Exception { server.requestHandler(req -> { assertEquals(HttpMethod.CONNECT, req.method()); assertEquals("whatever.com", req.host()); assertNull(req.path()); assertNull(req.query()); assertNull(req.scheme()); assertNull(req.uri()); assertNull(req.absoluteURI()); testComplete(); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2Headers headers = new DefaultHttp2Headers().method("CONNECT").authority("whatever.com"); request.encoder.writeHeaders(request.context, id, headers, 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testServerRequestPauseResume() throws Exception { testStreamPauseResume(req -> req); } private void testStreamPauseResume(Function<HttpServerRequest, ReadStream<Buffer>> streamProvider) throws Exception { Buffer expected = Buffer.buffer(); String chunk = TestUtils.randomAlphaString(1000); AtomicBoolean done = new AtomicBoolean(); AtomicBoolean paused = new AtomicBoolean(); Buffer received = Buffer.buffer(); server.requestHandler(req -> { ReadStream<Buffer> stream = streamProvider.apply(req); vertx.setPeriodic(1, timerID -> { if (paused.get()) { vertx.cancelTimer(timerID); done.set(true); // Let some time to accumulate some more buffers vertx.setTimer(100, id -> { stream.resume(); }); } }); stream.handler(received::appendBuffer); stream.endHandler(v -> { assertEquals(expected, received); testComplete(); }); stream.pause(); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, POST("/form").set("content-type", "text/plain"), 0, false, request.context.newPromise()); request.context.flush(); Http2Stream stream = request.connection.stream(id); class Anonymous { void send() { boolean writable = request.encoder.flowController().isWritable(stream); if (writable) { Buffer buf = Buffer.buffer(chunk); expected.appendBuffer(buf); request.encoder.writeData(request.context, id, buf.getByteBuf(), 0, false, request.context.newPromise()); request.context.flush(); request.context.executor().execute(this::send); } else { request.encoder.writeData(request.context, id, Unpooled.EMPTY_BUFFER, 0, true, request.context.newPromise()); request.context.flush(); paused.set(true); } } } new Anonymous().send(); }); fut.sync(); await(); } @Test public void testServerResponseWritability() throws Exception { testStreamWritability(req -> { HttpServerResponse resp = req.response(); resp.putHeader("content-type", "text/plain"); resp.setChunked(true); return resp; }); } private void testStreamWritability(Function<HttpServerRequest, WriteStream<Buffer>> streamProvider) throws Exception { Context ctx = vertx.getOrCreateContext(); String content = TestUtils.randomAlphaString(1024); StringBuilder expected = new StringBuilder(); Future<Void> whenFull = Future.future(); AtomicBoolean drain = new AtomicBoolean(); server.requestHandler(req -> { WriteStream<Buffer> stream = streamProvider.apply(req); vertx.setPeriodic(1, timerID -> { if (stream.writeQueueFull()) { stream.drainHandler(v -> { assertOnIOContext(ctx); expected.append("last"); stream.end(Buffer.buffer("last")); }); vertx.cancelTimer(timerID); drain.set(true); whenFull.complete(); } else { expected.append(content); Buffer buf = Buffer.buffer(content); stream.write(buf); } }); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { AtomicInteger toAck = new AtomicInteger(); int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.decoder.frameListener(new Http2FrameAdapter() { StringBuilder received = new StringBuilder(); @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { received.append(data.toString(StandardCharsets.UTF_8)); int delta = super.onDataRead(ctx, streamId, data, padding, endOfStream); if (endOfStream) { vertx.runOnContext(v -> { assertEquals(expected.toString(), received.toString()); testComplete(); }); return delta; } else { if (drain.get()) { return delta; } else { toAck.getAndAdd(delta); return 0; } } } }); whenFull.setHandler(ar -> { request.context.executor().execute(() -> { try { request.decoder.flowController().consumeBytes(request.connection.stream(id), toAck.intValue()); request.context.flush(); } catch (Http2Exception e) { e.printStackTrace(); fail(e); } }); }); }); fut.sync(); 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(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(new Http2EventAdapter() { int count; @Override public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception { switch (count++) { case 0: vertx.runOnContext(v -> { assertFalse(endStream); }); break; case 1: vertx.runOnContext(v -> { assertEquals("foo_value", headers.get("foo").toString()); assertEquals(1, headers.getAll("foo").size()); assertEquals("foo_value", headers.getAll("foo").get(0).toString()); assertEquals("bar_value", headers.getAll("bar").get(0).toString()); assertEquals(2, headers.getAll("juu").size()); assertEquals("juu_value_1", headers.getAll("juu").get(0).toString()); assertEquals("juu_value_2", headers.getAll("juu").get(1).toString()); assertTrue(endStream); testComplete(); }); break; default: vertx.runOnContext(v -> { fail(); }); break; } } }); int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testServerResetClientStream() throws Exception { server.requestHandler(req -> { req.handler(buf -> { req.response().reset(8); }); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.decoder.frameListener(new Http2EventAdapter() { @Override public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception { vertx.runOnContext(v -> { assertEquals(8, errorCode); testComplete(); }); } }); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, GET("/"), 0, false, request.context.newPromise()); encoder.writeData(request.context, id, Buffer.buffer("hello").getByteBuf(), 0, false, request.context.newPromise()); }); fut.sync(); await(); } @Test public void testClientResetServerStream() throws Exception { Context ctx = vertx.getOrCreateContext(); Future<Void> bufReceived = Future.future(); AtomicInteger resetCount = new AtomicInteger(); server.requestHandler(req -> { req.handler(buf -> { bufReceived.complete(); }); req.exceptionHandler(err -> { assertOnIOContext(ctx); assertTrue(err instanceof StreamResetException); assertEquals(10L, ((StreamResetException) err).getCode()); assertEquals(0, resetCount.getAndIncrement()); }); req.response().exceptionHandler(err -> { assertOnIOContext(ctx); assertTrue(err instanceof StreamResetException); assertEquals(10L, ((StreamResetException) err).getCode()); assertEquals(1, resetCount.getAndIncrement()); }); req.endHandler(v -> { assertOnIOContext(ctx); assertEquals(2, resetCount.get()); testComplete(); }); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, GET("/"), 0, false, request.context.newPromise()); encoder.writeData(request.context, id, Buffer.buffer("hello").getByteBuf(), 0, false, request.context.newPromise()); bufReceived.setHandler(ar -> { encoder.writeRstStream(request.context, id, 10, request.context.newPromise()); request.context.flush(); }); }); fut.sync(); await(); } @Test public void testConnectionClose() throws Exception { Context ctx = vertx.getOrCreateContext(); server.requestHandler(req -> { HttpConnection conn = req.connection(); conn.closeHandler(v -> { assertSame(ctx, Vertx.currentContext()); testComplete(); }); req.response().putHeader("Content-Type", "text/plain").end(); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.decoder.frameListener(new Http2FrameAdapter() { @Override public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception { request.context.close(); } }); }); fut.sync(); await(); } @Test public void testPushPromise() throws Exception { testPushPromise(GET("/").authority("whatever.com"), (resp, handler) -> { resp.push(HttpMethod.GET, "/wibble", handler); }, headers -> { assertEquals("GET", headers.method().toString()); assertEquals("https", headers.scheme().toString()); assertEquals("/wibble", headers.path().toString()); assertEquals("whatever.com", headers.authority().toString()); }); } @Test public void testPushPromiseHeaders() throws Exception { testPushPromise(GET("/").authority("whatever.com"), (resp, handler) -> { resp.push(HttpMethod.GET, "/wibble", MultiMap.caseInsensitiveMultiMap().set("foo", "foo_value") .set("bar", Arrays.<CharSequence>asList("bar_value_1", "bar_value_2")), handler); }, headers -> { assertEquals("GET", headers.method().toString()); assertEquals("https", headers.scheme().toString()); assertEquals("/wibble", headers.path().toString()); assertEquals(null, headers.authority()); assertEquals("foo_value", headers.get("foo").toString()); assertEquals(Arrays.asList("bar_value_1", "bar_value_2"), headers.getAll("bar").stream().map(CharSequence::toString).collect(Collectors.toList())); }); } @Test public void testPushPromiseNoAuthority() throws Exception { Http2Headers get = GET("/"); get.remove("authority"); testPushPromise(get, (resp, handler) -> { resp.push(HttpMethod.GET, "/wibble", handler); }, headers -> { assertEquals("GET", headers.method().toString()); assertEquals("https", headers.scheme().toString()); assertEquals("/wibble", headers.path().toString()); assertEquals(DEFAULT_HTTPS_HOST_AND_PORT, headers.authority().toString()); }); } @Test public void testPushPromiseOverrideAuthority() throws Exception { testPushPromise(GET("/").authority("whatever.com"), (resp, handler) -> { resp.push(HttpMethod.GET, "override.com", "/wibble", handler); }, headers -> { assertEquals("GET", headers.method().toString()); assertEquals("https", headers.scheme().toString()); assertEquals("/wibble", headers.path().toString()); assertEquals("override.com", headers.authority().toString()); }); } @Test public void testPushPromiseOverrideAuthorityWithNull() throws Exception { testPushPromise(GET("/").authority("whatever.com"), (resp, handler) -> { resp.push(HttpMethod.GET, null, "/wibble", handler); }, headers -> { assertEquals("GET", headers.method().toString()); assertEquals("https", headers.scheme().toString()); assertEquals("/wibble", headers.path().toString()); assertEquals(null, headers.authority()); }); } private void testPushPromise(Http2Headers requestHeaders, BiConsumer<HttpServerResponse, Handler<AsyncResult<HttpServerResponse>>> pusher, Consumer<Http2Headers> headerChecker) throws Exception { Context ctx = vertx.getOrCreateContext(); server.requestHandler(req -> { Handler<AsyncResult<HttpServerResponse>> handler = ar -> { assertSame(ctx, Vertx.currentContext()); assertTrue(ar.succeeded()); HttpServerResponse response = ar.result(); response./*putHeader("content-type", "application/plain").*/end("the_content"); assertIllegalStateException(() -> response.push(HttpMethod.GET, "/wibble2", resp -> { })); }; pusher.accept(req.response(), handler); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, requestHeaders, 0, true, request.context.newPromise()); Map<Integer, Http2Headers> pushed = new HashMap<>(); request.decoder.frameListener(new Http2FrameAdapter() { @Override public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers, int padding) throws Http2Exception { pushed.put(promisedStreamId, headers); } @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { int delta = super.onDataRead(ctx, streamId, data, padding, endOfStream); String content = data.toString(StandardCharsets.UTF_8); vertx.runOnContext(v -> { assertEquals(Collections.singleton(streamId), pushed.keySet()); assertEquals("the_content", content); Http2Headers pushedHeaders = pushed.get(streamId); headerChecker.accept(pushedHeaders); testComplete(); }); return delta; } }); }); fut.sync(); await(); } @Test public void testResetActivePushPromise() throws Exception { Context ctx = vertx.getOrCreateContext(); server.requestHandler(req -> { req.response().push(HttpMethod.GET, "/wibble", ar -> { assertTrue(ar.succeeded()); assertOnIOContext(ctx); HttpServerResponse response = ar.result(); response.exceptionHandler(err -> { testComplete(); }); response.setChunked(true).write("some_content"); }); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.decoder.frameListener(new Http2FrameAdapter() { @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { request.encoder.writeRstStream(ctx, streamId, Http2Error.CANCEL.code(), ctx.newPromise()); request.context.flush(); return super.onDataRead(ctx, streamId, data, padding, endOfStream); } }); }); fut.sync(); await(); } @Test public void testQueuePushPromise() throws Exception { Context ctx = vertx.getOrCreateContext(); int numPushes = 10; Set<String> pushSent = new HashSet<>(); server.requestHandler(req -> { req.response().setChunked(true).write("abc"); for (int i = 0; i < numPushes; i++) { int val = i; String path = "/wibble" + val; req.response().push(HttpMethod.GET, path, ar -> { assertTrue(ar.succeeded()); assertSame(ctx, Vertx.currentContext()); pushSent.add(path); vertx.setTimer(10, id -> { ar.result().end("wibble-" + val); }); }); } }); startServer(ctx); TestClient client = new TestClient(); client.settings.maxConcurrentStreams(3); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.decoder.frameListener(new Http2FrameAdapter() { int count = numPushes; Set<String> pushReceived = new HashSet<>(); @Override public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers, int padding) throws Http2Exception { pushReceived.add(headers.path().toString()); } @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { if (count-- == 0) { vertx.runOnContext(v -> { assertEquals(numPushes, pushSent.size()); assertEquals(pushReceived, pushSent); testComplete(); }); } return super.onDataRead(ctx, streamId, data, padding, endOfStream); } }); }); fut.sync(); await(); } @Test public void testResetPendingPushPromise() throws Exception { Context ctx = vertx.getOrCreateContext(); server.requestHandler(req -> { req.response().push(HttpMethod.GET, "/wibble", ar -> { assertFalse(ar.succeeded()); assertOnIOContext(ctx); testComplete(); }); }); startServer(ctx); TestClient client = new TestClient(); client.settings.maxConcurrentStreams(0); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.decoder.frameListener(new Http2FrameAdapter() { @Override public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers, int padding) throws Http2Exception { request.encoder.writeRstStream(request.context, promisedStreamId, Http2Error.CANCEL.code(), request.context.newPromise()); request.context.flush(); } }); }); fut.sync(); await(); } @Test public void testMissingMethodPseudoHeader() throws Exception { testMalformedRequestHeaders(new DefaultHttp2Headers().scheme("http").path("/")); } @Test public void testMissingSchemePseudoHeader() throws Exception { testMalformedRequestHeaders(new DefaultHttp2Headers().method("GET").path("/")); } @Test public void testMissingPathPseudoHeader() throws Exception { testMalformedRequestHeaders(new DefaultHttp2Headers().method("GET").scheme("http")); } @Test public void testInvalidAuthority() throws Exception { testMalformedRequestHeaders(new DefaultHttp2Headers().method("GET").scheme("http") .authority("foo@" + DEFAULT_HTTPS_HOST_AND_PORT).path("/")); } @Test public void testConnectInvalidPath() throws Exception { testMalformedRequestHeaders( new DefaultHttp2Headers().method("CONNECT").path("/").authority(DEFAULT_HTTPS_HOST_AND_PORT)); } @Test public void testConnectInvalidScheme() throws Exception { testMalformedRequestHeaders( new DefaultHttp2Headers().method("CONNECT").scheme("http").authority(DEFAULT_HTTPS_HOST_AND_PORT)); } @Test public void testConnectInvalidAuthority() throws Exception { testMalformedRequestHeaders( new DefaultHttp2Headers().method("CONNECT").authority("foo@" + DEFAULT_HTTPS_HOST_AND_PORT)); } private void testMalformedRequestHeaders(Http2Headers headers) throws Exception { server.requestHandler(req -> fail()); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, headers, 0, true, request.context.newPromise()); request.decoder.frameListener(new Http2FrameAdapter() { @Override public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception { vertx.runOnContext(v -> { testComplete(); }); } }); }); fut.sync(); await(); } @Test public void testRequestHandlerFailure() throws Exception { testHandlerFailure(false, (err, server) -> { server.requestHandler(req -> { throw err; }); }); } @Test public void testRequestEndHandlerFailure() throws Exception { testHandlerFailure(false, (err, server) -> { server.requestHandler(req -> { req.endHandler(v -> { throw err; }); }); }); } @Test public void testRequestEndHandlerFailureWithData() throws Exception { testHandlerFailure(true, (err, server) -> { server.requestHandler(req -> { req.endHandler(v -> { throw err; }); }); }); } @Test public void testRequestDataHandlerFailure() throws Exception { testHandlerFailure(true, (err, server) -> { server.requestHandler(req -> { req.handler(buf -> { System.out.println("throwing from data"); throw err; }); }); }); } private void testHandlerFailure(boolean data, BiConsumer<RuntimeException, HttpServer> configurator) throws Exception { RuntimeException failure = new RuntimeException(); io.vertx.core.http.Http2Settings settings = TestUtils.randomHttp2Settings(); server.close(); server = vertx.createHttpServer(serverOptions.setInitialSettings(settings)); configurator.accept(failure, server); Context ctx = vertx.getOrCreateContext(); ctx.exceptionHandler(err -> { assertSame(err, failure); testComplete(); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, GET("/"), 0, !data, request.context.newPromise()); if (data) { request.encoder.writeData(request.context, id, Buffer.buffer("hello").getByteBuf(), 0, true, request.context.newPromise()); } }); fut.sync(); await(); } private static File createTempFile(Buffer buffer) throws Exception { File f = File.createTempFile("vertx", ".bin"); f.deleteOnExit(); try (FileOutputStream out = new FileOutputStream(f)) { out.write(buffer.getBytes()); } return f; } @Test public void testSendFile() throws Exception { Buffer expected = Buffer.buffer(TestUtils.randomAlphaString(1000 * 1000)); File tmp = createTempFile(expected); testSendFile(expected, tmp.getAbsolutePath(), 0, expected.length()); } @Test public void testSendFileRange() throws Exception { Buffer expected = Buffer.buffer(TestUtils.randomAlphaString(1000 * 1000)); File tmp = createTempFile(expected); int from = 200 * 1000; int to = 700 * 1000; testSendFile(expected.slice(from, to), tmp.getAbsolutePath(), from, to - from); } @Test public void testSendEmptyFile() throws Exception { Buffer expected = Buffer.buffer(); File tmp = createTempFile(expected); testSendFile(expected, tmp.getAbsolutePath(), 0, expected.length()); } private void testSendFile(Buffer expected, String path, long offset, long length) throws Exception { waitFor(2); server.requestHandler(req -> { HttpServerResponse resp = req.response(); resp.bodyEndHandler(v -> { assertEquals(resp.bytesWritten(), length); complete(); }); resp.sendFile(path, offset, length); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(new Http2EventAdapter() { Buffer buffer = Buffer.buffer(); Http2Headers responseHeaders; @Override public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception { responseHeaders = headers; } @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { buffer.appendBuffer(Buffer.buffer(data.duplicate())); if (endOfStream) { vertx.runOnContext(v -> { assertEquals("" + length, responseHeaders.get("content-length").toString()); assertEquals(expected, buffer); complete(); }); } return data.readableBytes() + padding; } }); int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testStreamError() throws Exception { waitFor(5); Future<Void> when = Future.future(); Context ctx = vertx.getOrCreateContext(); server.requestHandler(req -> { req.exceptionHandler(err -> { // Called twice : reset + close assertEquals(ctx, Vertx.currentContext()); complete(); }); req.response().exceptionHandler(err -> { assertEquals(ctx, Vertx.currentContext()); complete(); }); req.response().closeHandler(v -> { assertEquals(ctx, Vertx.currentContext()); complete(); }); req.response().endHandler(v -> { assertEquals(ctx, Vertx.currentContext()); complete(); }); when.complete(); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, GET("/"), 0, false, request.context.newPromise()); request.context.flush(); when.setHandler(ar -> { // 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 request.channel.write(Buffer.buffer(new byte[] { 0x00, 0x00, 0x12, 0x00, 0x08, 0x00, 0x00, 0x00, (byte) (id & 0xFF), 0x1F, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }).getByteBuf()); request.context.flush(); }); }); fut.sync(); await(); } @Test public void testPromiseStreamError() throws Exception { Context ctx = vertx.getOrCreateContext(); waitFor(3); Future<Void> when = Future.future(); server.requestHandler(req -> { req.response().push(HttpMethod.GET, "/wibble", ar -> { assertTrue(ar.succeeded()); assertOnIOContext(ctx); when.complete(); HttpServerResponse resp = ar.result(); resp.exceptionHandler(err -> { assertSame(ctx, Vertx.currentContext()); complete(); }); resp.closeHandler(v -> { assertSame(ctx, Vertx.currentContext()); complete(); }); resp.endHandler(v -> { assertSame(ctx, Vertx.currentContext()); complete(); }); resp.setChunked(true).write("whatever"); // Transition to half-closed remote }); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(new Http2EventAdapter() { @Override public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers, int padding) throws Http2Exception { when.setHandler(ar -> { Http2ConnectionEncoder encoder = request.encoder; encoder.frameWriter().writeHeaders(request.context, promisedStreamId, GET("/"), 0, false, request.context.newPromise()); request.context.flush(); }); } }); int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; encoder.writeHeaders(request.context, id, GET("/"), 0, false, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testConnectionDecodeError() throws Exception { Context ctx = vertx.getOrCreateContext(); waitFor(6); Future<Void> when = Future.future(); server.requestHandler(req -> { req.exceptionHandler(err -> { // Called twice : reset + close assertSame(ctx, Vertx.currentContext()); complete(); }); req.response().exceptionHandler(err -> { // Called once : reset assertSame(ctx, Vertx.currentContext()); complete(); }); req.response().closeHandler(v -> { // Called once : close assertSame(ctx, Vertx.currentContext()); complete(); }); req.response().endHandler(v -> { // Called once : close assertSame(ctx, Vertx.currentContext()); complete(); }); req.connection().exceptionHandler(err -> { assertSame(ctx, Vertx.currentContext()); complete(); }); when.complete(); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); Http2ConnectionEncoder encoder = request.encoder; when.setHandler(ar -> { encoder.frameWriter().writeRstStream(request.context, 10, 0, request.context.newPromise()); request.context.flush(); }); encoder.writeHeaders(request.context, id, GET("/"), 0, false, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testServerSendGoAwayNoError() throws Exception { waitFor(2); AtomicReference<HttpServerRequest> first = new AtomicReference<>(); AtomicInteger status = new AtomicInteger(); AtomicInteger closed = new AtomicInteger(); AtomicBoolean done = new AtomicBoolean(); Context ctx = vertx.getOrCreateContext(); Handler<HttpServerRequest> requestHandler = req -> { if (first.compareAndSet(null, req)) { req.exceptionHandler(err -> { assertTrue(done.get()); }); req.response().exceptionHandler(err -> { assertTrue(done.get()); }); } else { assertEquals(0, status.getAndIncrement()); req.exceptionHandler(err -> { closed.incrementAndGet(); }); req.response().exceptionHandler(err -> { closed.incrementAndGet(); }); HttpConnection conn = req.connection(); conn.shutdownHandler(v -> { assertTrue(done.get()); }); conn.closeHandler(v -> { assertTrue(done.get()); }); ctx.runOnContext(v1 -> { conn.goAway(0, first.get().response().streamId()); vertx.setTimer(300, timerID -> { assertEquals(1, status.getAndIncrement()); done.set(true); complete(); }); }); } }; testServerSendGoAway(requestHandler, 0); } @Test public void testServerSendGoAwayInteralError() throws Exception { waitFor(3); AtomicReference<HttpServerRequest> first = new AtomicReference<>(); AtomicInteger status = new AtomicInteger(); AtomicInteger closed = new AtomicInteger(); Handler<HttpServerRequest> requestHandler = req -> { if (first.compareAndSet(null, req)) { req.exceptionHandler(err -> { fail(); }); req.response().closeHandler(err -> { closed.incrementAndGet(); }); req.response().endHandler(err -> { closed.incrementAndGet(); }); } else { assertEquals(0, status.getAndIncrement()); req.exceptionHandler(err -> { closed.incrementAndGet(); }); req.response().closeHandler(err -> { closed.incrementAndGet(); }); req.response().endHandler(err -> { closed.incrementAndGet(); }); HttpConnection conn = req.connection(); conn.closeHandler(v -> { assertEquals(5, closed.get()); assertEquals(1, status.get()); complete(); }); conn.shutdownHandler(v -> { assertEquals(1, status.get()); complete(); }); conn.goAway(2, first.get().response().streamId()); } }; testServerSendGoAway(requestHandler, 2); } @Test public void testShutdownWithTimeout() throws Exception { waitFor(2); AtomicInteger closed = new AtomicInteger(); AtomicReference<HttpServerRequest> first = new AtomicReference<>(); AtomicInteger status = new AtomicInteger(); Handler<HttpServerRequest> requestHandler = req -> { if (first.compareAndSet(null, req)) { req.exceptionHandler(err -> { fail(); }); req.response().closeHandler(err -> { closed.incrementAndGet(); }); req.response().endHandler(err -> { closed.incrementAndGet(); }); } else { assertEquals(0, status.getAndIncrement()); req.exceptionHandler(err -> { fail(); }); req.response().closeHandler(err -> { closed.incrementAndGet(); }); req.response().endHandler(err -> { closed.incrementAndGet(); }); HttpConnection conn = req.connection(); conn.closeHandler(v -> { assertEquals(4, closed.get()); assertEquals(1, status.getAndIncrement()); complete(); }); conn.shutdown(300); } }; testServerSendGoAway(requestHandler, 0); } @Test public void testShutdown() throws Exception { waitFor(2); AtomicReference<HttpServerRequest> first = new AtomicReference<>(); AtomicInteger status = new AtomicInteger(); Handler<HttpServerRequest> requestHandler = req -> { if (first.compareAndSet(null, req)) { req.exceptionHandler(err -> { fail(); }); req.response().exceptionHandler(err -> { fail(); }); } else { assertEquals(0, status.getAndIncrement()); req.exceptionHandler(err -> { fail(); }); req.response().exceptionHandler(err -> { fail(); }); HttpConnection conn = req.connection(); conn.closeHandler(v -> { assertEquals(2, status.getAndIncrement()); complete(); }); conn.shutdown(); vertx.setTimer(300, timerID -> { assertEquals(1, status.getAndIncrement()); first.get().response().end(); req.response().end(); }); } }; testServerSendGoAway(requestHandler, 0); } private void testServerSendGoAway(Handler<HttpServerRequest> requestHandler, int expectedError) throws Exception { server.requestHandler(requestHandler); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(new Http2EventAdapter() { @Override public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) throws Http2Exception { vertx.runOnContext(v -> { assertEquals(expectedError, errorCode); complete(); }); } }); Http2ConnectionEncoder encoder = request.encoder; int id1 = request.nextStreamId(); encoder.writeHeaders(request.context, id1, GET("/"), 0, true, request.context.newPromise()); int id2 = request.nextStreamId(); encoder.writeHeaders(request.context, id2, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testServerClose() throws Exception { waitFor(2); AtomicInteger status = new AtomicInteger(); Handler<HttpServerRequest> requestHandler = req -> { HttpConnection conn = req.connection(); conn.shutdownHandler(v -> { assertEquals(0, status.getAndIncrement()); }); conn.closeHandler(v -> { assertEquals(1, status.getAndIncrement()); complete(); }); conn.close(); }; server.requestHandler(requestHandler); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.channel.closeFuture().addListener(v1 -> { vertx.runOnContext(v2 -> { complete(); }); }); request.decoder.frameListener(new Http2EventAdapter() { @Override public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) throws Http2Exception { vertx.runOnContext(v -> { assertEquals(0, errorCode); }); } }); Http2ConnectionEncoder encoder = request.encoder; int id = request.nextStreamId(); encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testClientSendGoAwayNoError() throws Exception { Future<Void> abc = Future.future(); Context ctx = vertx.getOrCreateContext(); Handler<HttpServerRequest> requestHandler = req -> { HttpConnection conn = req.connection(); AtomicInteger numShutdown = new AtomicInteger(); AtomicBoolean completed = new AtomicBoolean(); conn.shutdownHandler(v -> { assertOnIOContext(ctx); numShutdown.getAndIncrement(); vertx.setTimer(100, timerID -> { // Delay so we can check the connection is not closed completed.set(true); testComplete(); }); }); conn.goAwayHandler(ga -> { assertOnIOContext(ctx); assertEquals(0, numShutdown.get()); req.response().end(); }); conn.closeHandler(v -> { assertTrue(completed.get()); }); abc.complete(); }; server.requestHandler(requestHandler); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { Http2ConnectionEncoder encoder = request.encoder; int id = request.nextStreamId(); encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); abc.setHandler(ar -> { encoder.writeGoAway(request.context, id, 0, Unpooled.EMPTY_BUFFER, request.context.newPromise()); request.context.flush(); }); }); fut.sync(); await(); } @Test public void testClientSendGoAwayInternalError() throws Exception { Future<Void> abc = Future.future(); Context ctx = vertx.getOrCreateContext(); Handler<HttpServerRequest> requestHandler = req -> { HttpConnection conn = req.connection(); AtomicInteger status = new AtomicInteger(); conn.goAwayHandler(ga -> { assertOnIOContext(ctx); assertEquals(0, status.getAndIncrement()); req.response().end(); }); conn.shutdownHandler(v -> { assertOnIOContext(ctx); assertEquals(1, status.getAndIncrement()); }); conn.closeHandler(v -> { assertEquals(2, status.getAndIncrement()); testComplete(); }); abc.complete(); }; server.requestHandler(requestHandler); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { Http2ConnectionEncoder encoder = request.encoder; int id = request.nextStreamId(); encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); abc.setHandler(ar -> { encoder.writeGoAway(request.context, id, 3, Unpooled.EMPTY_BUFFER, request.context.newPromise()); request.context.flush(); }); }); fut.sync(); await(); } @Test public void testShutdownOverride() throws Exception { AtomicLong shutdown = new AtomicLong(); Handler<HttpServerRequest> requestHandler = req -> { HttpConnection conn = req.connection(); shutdown.set(System.currentTimeMillis()); conn.shutdown(10000); vertx.setTimer(300, v -> { conn.shutdown(300); }); }; server.requestHandler(requestHandler); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.channel.closeFuture().addListener(v1 -> { vertx.runOnContext(v2 -> { assertTrue(shutdown.get() - System.currentTimeMillis() < 1200); testComplete(); }); }); Http2ConnectionEncoder encoder = request.encoder; int id = request.nextStreamId(); encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testRequestResponseLifecycle() throws Exception { waitFor(2); server.requestHandler(req -> { req.endHandler(v -> { assertIllegalStateException(() -> req.setExpectMultipart(false)); assertIllegalStateException(() -> req.handler(buf -> { })); assertIllegalStateException(() -> req.uploadHandler(upload -> { })); assertIllegalStateException(() -> req.endHandler(v2 -> { })); complete(); }); HttpServerResponse resp = req.response(); resp.setChunked(true).write(Buffer.buffer("whatever")); assertTrue(resp.headWritten()); assertIllegalStateException(() -> resp.setChunked(false)); assertIllegalStateException(() -> resp.setStatusCode(100)); assertIllegalStateException(() -> resp.setStatusMessage("whatever")); assertIllegalStateException(() -> resp.putHeader("a", "b")); assertIllegalStateException(() -> resp.putHeader("a", (CharSequence) "b")); assertIllegalStateException(() -> resp.putHeader("a", (Iterable<String>) Arrays.asList("a", "b"))); assertIllegalStateException(() -> resp.putHeader("a", (Arrays.<CharSequence>asList("a", "b")))); assertIllegalStateException(resp::writeContinue); resp.end(); assertIllegalStateException(() -> resp.write("a")); assertIllegalStateException(() -> resp.write("a", "UTF-8")); assertIllegalStateException(() -> resp.write(Buffer.buffer("a"))); assertIllegalStateException(resp::end); assertIllegalStateException(() -> resp.end("a")); assertIllegalStateException(() -> resp.end("a", "UTF-8")); assertIllegalStateException(() -> resp.end(Buffer.buffer("a"))); assertIllegalStateException(() -> resp.sendFile("the-file.txt")); assertIllegalStateException(() -> resp.reset(0)); assertIllegalStateException(() -> resp.closeHandler(v -> { })); assertIllegalStateException(() -> resp.endHandler(v -> { })); assertIllegalStateException(() -> resp.drainHandler(v -> { })); assertIllegalStateException(() -> resp.exceptionHandler(err -> { })); assertIllegalStateException(resp::writeQueueFull); assertIllegalStateException(() -> resp.setWriteQueueMaxSize(100)); assertIllegalStateException(() -> resp.putTrailer("a", "b")); assertIllegalStateException(() -> resp.putTrailer("a", (CharSequence) "b")); assertIllegalStateException(() -> resp.putTrailer("a", (Iterable<String>) Arrays.asList("a", "b"))); assertIllegalStateException(() -> resp.putTrailer("a", (Arrays.<CharSequence>asList("a", "b")))); assertIllegalStateException(() -> resp.push(HttpMethod.GET, "/whatever", ar -> { })); complete(); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testResponseCompressionDisabled() throws Exception { waitFor(2); String expected = TestUtils.randomAlphaString(1000); server.requestHandler(req -> { req.response().end(expected); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(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 -> { assertEquals(null, headers.get(HttpHeaderNames.CONTENT_ENCODING)); complete(); }); } @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { String s = data.toString(StandardCharsets.UTF_8); vertx.runOnContext(v -> { assertEquals(expected, s); complete(); }); return super.onDataRead(ctx, streamId, data, padding, endOfStream); } }); int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, GET("/").add("accept-encoding", "gzip"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testResponseCompressionEnabled() throws Exception { waitFor(2); String expected = TestUtils.randomAlphaString(1000); server.close(); server = vertx.createHttpServer(serverOptions.setCompressionSupported(true)); server.requestHandler(req -> { req.response().end(expected); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(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 -> { assertEquals("gzip", headers.get(HttpHeaderNames.CONTENT_ENCODING).toString()); complete(); }); } @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { byte[] bytes = new byte[data.readableBytes()]; data.readBytes(bytes); vertx.runOnContext(v -> { String decoded; try { GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(bytes)); ByteArrayOutputStream baos = new ByteArrayOutputStream(); while (true) { int i = in.read(); if (i == -1) { break; } baos.write(i); ; } decoded = baos.toString(); } catch (IOException e) { fail(e); return; } assertEquals(expected, decoded); complete(); }); return super.onDataRead(ctx, streamId, data, padding, endOfStream); } }); int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, GET("/").add("accept-encoding", "gzip"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testRequestCompressionEnabled() throws Exception { String expected = TestUtils.randomAlphaString(1000); byte[] expectedGzipped = TestUtils.compressGzip(expected); server.close(); server = vertx.createHttpServer(serverOptions.setDecompressionSupported(true)); server.requestHandler(req -> { StringBuilder postContent = new StringBuilder(); req.handler(buff -> { postContent.append(buff.toString()); }); req.endHandler(v -> { req.response().putHeader("content-type", "text/plain").end(""); assertEquals(expected, postContent.toString()); testComplete(); }); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, POST("/").add("content-encoding", "gzip"), 0, false, request.context.newPromise()); request.encoder.writeData(request.context, id, Buffer.buffer(expectedGzipped).getByteBuf(), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void test100ContinueHandledManually() throws Exception { server.requestHandler(req -> { assertEquals("100-continue", req.getHeader("expect")); HttpServerResponse resp = req.response(); resp.writeContinue(); req.bodyHandler(body -> { assertEquals("the-body", body.toString()); resp.putHeader("wibble", "wibble-value").end(); }); }); test100Continue(); } @Test public void test100ContinueHandledAutomatically() throws Exception { server.close(); server = vertx.createHttpServer(serverOptions.setHandle100ContinueAutomatically(true)); server.requestHandler(req -> { HttpServerResponse resp = req.response(); req.bodyHandler(body -> { assertEquals("the-body", body.toString()); resp.putHeader("wibble", "wibble-value").end(); }); }); test100Continue(); } private void test100Continue() throws Exception { startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.decoder.frameListener(new Http2EventAdapter() { int count = 0; @Override public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception { switch (count++) { case 0: vertx.runOnContext(v -> { assertEquals("100", headers.status().toString()); }); request.encoder.writeData(request.context, id, Buffer.buffer("the-body").getByteBuf(), 0, true, request.context.newPromise()); request.context.flush(); break; case 1: vertx.runOnContext(v -> { assertEquals("200", headers.status().toString()); assertEquals("wibble-value", headers.get("wibble").toString()); testComplete(); }); break; default: vertx.runOnContext(v -> { fail(); }); } } }); request.encoder.writeHeaders(request.context, id, GET("/").add("expect", "100-continue"), 0, false, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void test100ContinueRejectedManually() throws Exception { server.requestHandler(req -> { req.response().setStatusCode(405).end(); req.handler(buf -> { fail(); }); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.decoder.frameListener(new Http2EventAdapter() { int count = 0; @Override public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception { switch (count++) { case 0: vertx.runOnContext(v -> { assertEquals("405", headers.status().toString()); vertx.setTimer(100, v2 -> { testComplete(); }); }); break; default: vertx.runOnContext(v -> { fail(); }); } } }); request.encoder.writeHeaders(request.context, id, GET("/").add("expect", "100-continue"), 0, false, request.context.newPromise()); request.context.flush(); }); fut.sync(); 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(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.decoder.frameListener(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 -> { assertEquals("200", headers.status().toString()); assertFalse(endStream); }); request.encoder.writeData(request.context, id, Buffer.buffer("some-data").getByteBuf(), 0, false, request.context.newPromise()); request.context.flush(); } StringBuilder received = new StringBuilder(); @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { String s = data.toString(StandardCharsets.UTF_8); received.append(s); if (received.toString().equals("some-data")) { received.setLength(0); vertx.runOnContext(v -> { assertFalse(endOfStream); }); request.encoder.writeData(request.context, id, Buffer.buffer("last-data").getByteBuf(), 0, true, request.context.newPromise()); } else if (endOfStream) { vertx.runOnContext(v -> { assertEquals("last-data", received.toString()); complete(); }); } return data.readableBytes() + padding; } }); request.encoder.writeHeaders(request.context, id, GET("/"), 0, false, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testNetSocketSendFile() throws Exception { Buffer expected = Buffer.buffer(TestUtils.randomAlphaString(1000 * 1000)); File tmp = createTempFile(expected); testNetSocketSendFile(expected, tmp.getAbsolutePath(), 0, expected.length()); } @Test public void testNetSocketSendFileRange() throws Exception { Buffer expected = Buffer.buffer(TestUtils.randomAlphaString(1000 * 1000)); File tmp = createTempFile(expected); int from = 200 * 1000; int to = 700 * 1000; testNetSocketSendFile(expected.slice(from, to), tmp.getAbsolutePath(), from, to - from); } private void testNetSocketSendFile(Buffer expected, String path, long offset, long length) throws Exception { server.requestHandler(req -> { NetSocket socket = req.netSocket(); socket.sendFile(path, offset, length, ar -> { assertTrue(ar.succeeded()); socket.end(); }); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.decoder.frameListener(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 -> { assertEquals("200", headers.status().toString()); assertFalse(endStream); }); } Buffer received = Buffer.buffer(); @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { received.appendBuffer(Buffer.buffer(data.copy())); if (endOfStream) { vertx.runOnContext(v -> { assertEquals(received, expected); testComplete(); }); } return data.readableBytes() + padding; } }); request.encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); 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.write(buff); socket.close(); 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(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.decoder.frameListener(new Http2EventAdapter() { int count = 0; @Override public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception { int c = count++; vertx.runOnContext(v -> { assertEquals(0, c); }); request.encoder.writeData(request.context, id, Buffer.buffer("some-data").getByteBuf(), 0, false, request.context.newPromise()); request.context.flush(); } StringBuilder received = new StringBuilder(); @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { String s = data.toString(StandardCharsets.UTF_8); received.append(s); if (endOfStream) { request.encoder.writeData(request.context, id, Buffer.buffer("last-data").getByteBuf(), 0, true, request.context.newPromise()); vertx.runOnContext(v -> { assertEquals("some-data", received.toString()); complete(); }); } return data.readableBytes() + padding; } }); request.encoder.writeHeaders(request.context, id, GET("/"), 0, false, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testNetSocketHandleReset() throws Exception { server.requestHandler(req -> { NetSocket socket = req.netSocket(); AtomicInteger status = new AtomicInteger(); socket.exceptionHandler(err -> { assertTrue(err instanceof StreamResetException); StreamResetException ex = (StreamResetException) err; assertEquals(0, ex.getCode()); assertEquals(0, status.getAndIncrement()); }); socket.endHandler(v -> { fail(); }); socket.closeHandler(v -> { assertEquals(1, status.getAndIncrement()); testComplete(); }); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.decoder.frameListener(new Http2EventAdapter() { int count = 0; @Override public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception { int c = count++; vertx.runOnContext(v -> { assertEquals(0, c); }); request.encoder.writeRstStream(ctx, streamId, 0, ctx.newPromise()); request.context.flush(); } }); request.encoder.writeHeaders(request.context, id, GET("/"), 0, false, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testNetSocketPauseResume() throws Exception { testStreamPauseResume(HttpServerRequest::netSocket); } @Test public void testNetSocketWritability() throws Exception { testStreamWritability(HttpServerRequest::netSocket); } @Test public void testUnknownFrame() throws Exception { Buffer expectedSend = TestUtils.randomBuffer(500); Buffer expectedRecv = TestUtils.randomBuffer(500); Context ctx = vertx.getOrCreateContext(); server.requestHandler(req -> { req.customFrameHandler(frame -> { assertOnIOContext(ctx); assertEquals(10, frame.type()); assertEquals(253, frame.flags()); assertEquals(expectedSend, frame.payload()); req.response().writeCustomFrame(12, 134, expectedRecv).end(); }); }); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.decoder.frameListener(new Http2EventAdapter() { int status = 0; @Override public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception { int s = status++; vertx.runOnContext(v -> { assertEquals(0, s); }); } @Override public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, ByteBuf payload) { int s = status++; Buffer recv = Buffer.buffer(payload.copy()); vertx.runOnContext(v -> { assertEquals(1, s); assertEquals(12, frameType); assertEquals(134, flags.value()); assertEquals(expectedRecv, recv); }); } @Override public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { int len = data.readableBytes(); int s = status++; vertx.runOnContext(v -> { assertEquals(2, s); assertEquals(0, len); assertTrue(endOfStream); testComplete(); }); return data.readableBytes() + padding; } }); request.encoder.writeHeaders(request.context, id, GET("/"), 0, false, request.context.newPromise()); request.encoder.writeFrame(request.context, (byte) 10, id, new Http2Flags((short) 253), expectedSend.getByteBuf(), request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testUpgradeToClearText() throws Exception { server.close(); server = vertx.createHttpServer( serverOptions.setHost(DEFAULT_HTTP_HOST).setPort(DEFAULT_HTTP_PORT).setUseAlpn(false).setSsl(false) .setInitialSettings(new io.vertx.core.http.Http2Settings().setMaxConcurrentStreams(20000))); server.requestHandler(req -> { assertEquals(HttpVersion.HTTP_2, req.version()); assertEquals(10000, req.connection().remoteSettings().getMaxConcurrentStreams()); assertFalse(req.isSSL()); req.response().end(); }); startServer(); client = vertx.createHttpClient(clientOptions.setUseAlpn(false).setSsl(false) .setInitialSettings(new io.vertx.core.http.Http2Settings().setMaxConcurrentStreams(10000))); HttpClientRequest req = client.get(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath"); req.handler(resp -> { assertEquals(HttpVersion.HTTP_2, resp.version()); assertEquals(20000, req.connection().remoteSettings().getMaxConcurrentStreams()); testComplete(); }).exceptionHandler(this::fail).end(); await(); } @Test public void testPushPromiseClearText() throws Exception { waitFor(2); server.close(); server = vertx.createHttpServer(serverOptions.setHost(DEFAULT_HTTP_HOST).setPort(DEFAULT_HTTP_PORT) .setUseAlpn(false).setSsl(false)); server.requestHandler(req -> { req.response().push(HttpMethod.GET, "/resource", ar -> { assertTrue(ar.succeeded()); ar.result().end("the-pushed-response"); }); req.response().end(); }); startServer(); client = vertx.createHttpClient(clientOptions.setUseAlpn(false).setSsl(false)); HttpClientRequest req = client.get(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath"); req.handler(resp -> { assertEquals(HttpVersion.HTTP_2, resp.version()); complete(); }).exceptionHandler(this::fail).pushHandler(pushedReq -> { assertEquals("http", pushedReq.headers().get(":scheme")); pushedReq.handler(pushResp -> { pushResp.bodyHandler(buff -> { assertEquals("the-pushed-response", buff.toString()); complete(); }); }); }).end(); await(); } @Test public void testUpgradeToClearTextInvalidConnectionHeader() throws Exception { testUpgradeFailure(vertx.getOrCreateContext(), client -> client.get(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath").putHeader("Upgrade", "h2c") .putHeader("Connection", "Upgrade").putHeader("HTTP2-Settings", "")); } @Test public void testUpgradeToClearTextMalformedSettings() throws Exception { testUpgradeFailure(vertx.getOrCreateContext(), client -> client.get(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath").putHeader("Upgrade", "h2c") .putHeader("Connection", "Upgrade,HTTP2-Settings") .putHeader("HTTP2-Settings", "incorrect-settings")); } @Test public void testUpgradeToClearTextInvalidSettings() throws Exception { Buffer buffer = Buffer.buffer(); buffer.appendUnsignedShort(5).appendUnsignedInt((0xFFFFFF + 1)); String s = new String(Base64.getUrlEncoder().encode(buffer.getBytes()), StandardCharsets.UTF_8); testUpgradeFailure(vertx.getOrCreateContext(), client -> client.get(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath").putHeader("Upgrade", "h2c") .putHeader("Connection", "Upgrade,HTTP2-Settings").putHeader("HTTP2-Settings", s)); } @Test public void testUpgradeToClearTextMissingSettings() throws Exception { testUpgradeFailure(vertx.getOrCreateContext(), client -> client.get(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath").putHeader("Upgrade", "h2c") .putHeader("Connection", "Upgrade,HTTP2-Settings")); } @Test public void testUpgradeToClearTextWorkerContext() throws Exception { testUpgradeFailure(createWorker(), client -> client.get(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath").putHeader("Upgrade", "h2c") .putHeader("Connection", "Upgrade,HTTP2-Settings").putHeader("HTTP2-Settings", "")); } private void testUpgradeFailure(Context context, Function<HttpClient, HttpClientRequest> doRequest) throws Exception { server.close(); server = vertx.createHttpServer(serverOptions.setHost(DEFAULT_HTTP_HOST).setPort(DEFAULT_HTTP_PORT) .setUseAlpn(false).setSsl(false)); server.requestHandler(req -> { fail(); }); startServer(context); client = vertx.createHttpClient( clientOptions.setProtocolVersion(HttpVersion.HTTP_1_1).setUseAlpn(false).setSsl(false)); doRequest.apply(client).handler(resp -> { assertEquals(400, resp.statusCode()); assertEquals(HttpVersion.HTTP_1_1, resp.version()); testComplete(); }).exceptionHandler(this::fail).end(); await(); } @Test public void testIdleTimeout() throws Exception { waitFor(5); server.close(); server = vertx.createHttpServer(serverOptions.setIdleTimeout(2)); server.requestHandler(req -> { req.exceptionHandler(err -> { assertTrue(err instanceof ClosedChannelException); complete(); }); req.response().closeHandler(v -> { complete(); }); req.response().endHandler(v -> { complete(); }); req.connection().closeHandler(v -> { complete(); }); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { int id = request.nextStreamId(); request.decoder.frameListener(new Http2EventAdapter() { }); request.encoder.writeHeaders(request.context, id, GET("/"), 0, false, request.context.newPromise()); request.context.flush(); }); fut.sync(); fut.channel().closeFuture().addListener(v1 -> { vertx.runOnContext(v2 -> { complete(); }); }); await(); } @Test public void testFallbackOnHttp1ForWorkerContext() throws Exception { server.requestHandler(req -> { assertEquals(HttpVersion.HTTP_1_1, req.version()); req.response().end(); }); startServer(createWorker()); client = vertx.createHttpClient(clientOptions); client.get(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/somepath", resp -> { assertEquals(HttpVersion.HTTP_1_1, resp.version()); testComplete(); }).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.ping(expected, ar -> { assertSame(ctx, Vertx.currentContext()); assertTrue(ar.succeeded()); assertEquals(expected, ar.result()); complete(); }); }); server.requestHandler(req -> fail()); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(new Http2EventAdapter() { @Override public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception { Buffer buffer = Buffer.buffer(data.copy()); vertx.runOnContext(v -> { assertEquals(expected, buffer); complete(); }); } }); }); fut.sync(); await(); } @Test public void testReceivePing() throws Exception { Buffer expected = TestUtils.randomBuffer(8); Context ctx = vertx.getOrCreateContext(); server.close(); server.connectionHandler(conn -> { conn.pingHandler(buff -> { assertOnIOContext(ctx); assertEquals(expected, buff); testComplete(); }); }); server.requestHandler(req -> fail()); startServer(ctx); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.encoder.writePing(request.context, false, expected.getByteBuf(), request.context.newPromise()); }); fut.sync(); await(); } @Test public void testPriorKnowledge() throws Exception { server.close(); server = vertx .createHttpServer(new HttpServerOptions().setPort(DEFAULT_HTTP_PORT).setHost(DEFAULT_HTTP_HOST)); server.requestHandler(req -> { req.response().end("Hello World"); }); startServer(); TestClient client = new TestClient() { @Override protected ChannelInitializer channelInitializer(int port, String host, Consumer<Connection> handler) { return new ChannelInitializer() { @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline p = ch.pipeline(); Http2Connection connection = new DefaultHttp2Connection(false); TestClientHandlerBuilder clientHandlerBuilder = new TestClientHandlerBuilder(handler); TestClientHandler clientHandler = clientHandlerBuilder.build(connection); p.addLast(clientHandler); } }; } }; ChannelFuture fut = client.connect(DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, request -> { request.decoder.frameListener(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 -> { testComplete(); }); } }); int id = request.nextStreamId(); request.encoder.writeHeaders(request.context, id, GET("/"), 0, true, request.context.newPromise()); request.context.flush(); }); fut.sync(); await(); } @Test public void testConnectionWindowSize() throws Exception { server.close(); server = vertx.createHttpServer(createHttp2ServerOptions(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST) .setHttp2ConnectionWindowSize(65535 + 65535)); server.requestHandler(req -> { req.response().end(); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(new Http2EventAdapter() { @Override public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) throws Http2Exception { vertx.runOnContext(v -> { assertEquals(65535, windowSizeIncrement); testComplete(); }); } }); }); fut.sync(); await(); } @Test public void testUpdateConnectionWindowSize() throws Exception { server.connectionHandler(conn -> { assertEquals(65535, conn.getWindowSize()); conn.setWindowSize(65535 + 10000); assertEquals(65535 + 10000, conn.getWindowSize()); conn.setWindowSize(65535 + 65535); assertEquals(65535 + 65535, conn.getWindowSize()); }).requestHandler(req -> { req.response().end(); }); startServer(); TestClient client = new TestClient(); ChannelFuture fut = client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, request -> { request.decoder.frameListener(new Http2EventAdapter() { @Override public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) throws Http2Exception { vertx.runOnContext(v -> { assertEquals(65535, windowSizeIncrement); testComplete(); }); } }); }); fut.sync(); await(); } }