From 9de6785fc838f2969f3c48b36d2ec85fe868f02e Mon Sep 17 00:00:00 2001 From: doxlik Date: Tue, 25 Aug 2026 16:16:04 +0400 Subject: [PATCH 1/3] Add a ChunkedWriteHandler when a socket is upgraded to SSL The ChunkedWriteHandler is installed once when the pipeline is created and only when the channel is encrypted at that point, whereas supportsFileRegion() is evaluated at write time against the live pipeline. A socket upgraded with upgradeToSsl() therefore stops using the file region transfer without gaining the handler that consumes the chunked file NetSocket#sendFile writes: the ChunkedNioFile reaches the tail of the pipeline and Netty rejects it with an UnsupportedMessageTypeException, the future fails and the peer receives no data. Install the handler when the upgrade completes, unless the pipeline already has one. It is added before the connection handler so that the chunks are written to the SSL handler and encrypted, which is where the pipeline places it when the channel is created encrypted. Signed-off-by: doxlik --- .../core/net/impl/tcp/NetSocketImpl.java | 6 ++ .../test/java/io/vertx/tests/net/NetTest.java | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java index a78cbd8137b..daeddeaadcf 100644 --- a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java @@ -17,6 +17,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.ssl.SslHandler; +import io.netty.handler.stream.ChunkedWriteHandler; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; @@ -169,6 +170,11 @@ private Future sslUpgrade(String serverName, SSLOptions sslOptions, ByteBu chctx.pipeline().addFirst("handshaker", handshaker); chctx.pipeline().addFirst("ssl", sslHandler); } + if (chctx.pipeline().get("chunkedWriter") == null) { + // The connection can no longer use zero-copy, sendFile needs a ChunkedWriteHandler to consume the + // chunked file it writes - the pipeline was set up without one since the channel was not encrypted + chctx.pipeline().addBefore(chctx.name(), "chunkedWriter", new ChunkedWriteHandler()); + } channelPromise.addListener(p); doPause(); } else { diff --git a/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java b/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java index c00503bef0e..9fba88b19b3 100755 --- a/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java @@ -2480,6 +2480,70 @@ public void sendFileServerToClient(Checkpoint checkpoint) throws Exception { sock.write("foo"); } + @Test + public void testSendFileAfterTlsUpgrade() throws Exception { + File dir = testFolder.newFolder(); + int size = 64 * 1024; + String content = String.valueOf('a').repeat(size); + File f = setupFile(dir.toString(), "upgraded.dat", content); + Promise sent = Promise.promise(); + server.connectHandler(socket -> { + socket.upgradeToSsl(new ServerSSLOptions().setKeyCertOptions(Cert.SERVER_JKS.get())) + .compose(v -> socket.sendFile(f.getAbsolutePath())) + .onComplete(sent); + }); + server.listen(1234, "localhost").await(); + NetSocket socket = client.connect(new ConnectOptions() + .setPort(1234) + .setHost("localhost") + .setSsl(true) + .setSslOptions(new ClientSSLOptions() + .setHostnameVerificationAlgorithm("") + .setTrustAll(true))).await(); + Buffer received = Buffer.buffer(); + Promise done = Promise.promise(); + socket.handler(buff -> { + received.appendBuffer(buff); + if (received.length() == size) { + done.tryComplete(); + } + }); + sent.future().await(); + done.future().await(); + socket.close().await(); + server.close().await(); + assertEquals(content, received.toString()); + } + + @Test + public void testSendFileFromClientAfterTlsUpgrade() throws Exception { + File dir = testFolder.newFolder(); + int size = 64 * 1024; + String content = String.valueOf('a').repeat(size); + File f = setupFile(dir.toString(), "upgraded-client.dat", content); + Buffer received = Buffer.buffer(); + Promise done = Promise.promise(); + server.connectHandler(socket -> { + socket.upgradeToSsl(new ServerSSLOptions().setKeyCertOptions(Cert.SERVER_JKS.get())) + .onSuccess(v -> socket.handler(buff -> { + received.appendBuffer(buff); + if (received.length() == size) { + done.tryComplete(); + } + })); + }); + server.listen(1234, "localhost").await(); + NetSocket socket = client.connect(1234, "localhost").await(); + socket.upgradeToSsl(new ClientSSLOptions() + .setHostnameVerificationAlgorithm("") + .setTrustAll(true)).await(); + socket.sendFile(f.getAbsolutePath()).await(); + done.future().await(); + socket.close().await(); + server.close().await(); + assertEquals(content, received.toString()); + } + @Test public void testSendFileDirectory() throws Exception { File fDir = testFolder.newFolder(); From 39bbbb0ab08b52b2ff1e3bf89f118a39470aa6af Mon Sep 17 00:00:00 2001 From: doxlik Date: Tue, 25 Aug 2026 16:16:04 +0400 Subject: [PATCH 2/3] Add a ChunkedWriteHandler when a socket is upgraded to SSL The ChunkedWriteHandler is installed once when the pipeline is created and only when the channel is encrypted at that point, whereas supportsFileRegion() is evaluated at write time against the live pipeline. A socket upgraded with upgradeToSsl() therefore stops using the file region transfer without gaining the handler that consumes the chunked file NetSocket#sendFile writes: the ChunkedNioFile reaches the tail of the pipeline and Netty rejects it with an UnsupportedMessageTypeException, the future fails and the peer receives no data. Install the handler when the upgrade completes, unless the pipeline already has one. It is added before the connection handler so that the chunks are written to the SSL handler and encrypted, which is where the pipeline places it when the channel is created encrypted. Signed-off-by: doxlik --- .../core/net/impl/tcp/NetSocketImpl.java | 6 ++ .../test/java/io/vertx/tests/net/NetTest.java | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java index a78cbd8137b..daeddeaadcf 100644 --- a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetSocketImpl.java @@ -17,6 +17,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.ssl.SslHandler; +import io.netty.handler.stream.ChunkedWriteHandler; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; @@ -169,6 +170,11 @@ private Future sslUpgrade(String serverName, SSLOptions sslOptions, ByteBu chctx.pipeline().addFirst("handshaker", handshaker); chctx.pipeline().addFirst("ssl", sslHandler); } + if (chctx.pipeline().get("chunkedWriter") == null) { + // The connection can no longer use zero-copy, sendFile needs a ChunkedWriteHandler to consume the + // chunked file it writes - the pipeline was set up without one since the channel was not encrypted + chctx.pipeline().addBefore(chctx.name(), "chunkedWriter", new ChunkedWriteHandler()); + } channelPromise.addListener(p); doPause(); } else { diff --git a/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java b/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java index f425e39fb28..fa878656f86 100755 --- a/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java @@ -2480,6 +2480,70 @@ public void sendFileServerToClient(Checkpoint checkpoint) throws Exception { sock.write("foo"); } + @Test + public void testSendFileAfterTlsUpgrade() throws Exception { + File dir = testFolder.newFolder(); + int size = 64 * 1024; + String content = String.valueOf('a').repeat(size); + File f = setupFile(dir.toString(), "upgraded.dat", content); + Promise sent = Promise.promise(); + server.connectHandler(socket -> { + socket.upgradeToSsl(new ServerSSLOptions().setKeyCertOptions(Cert.SERVER_JKS.get())) + .compose(v -> socket.sendFile(f.getAbsolutePath())) + .onComplete(sent); + }); + server.listen(1234, "localhost").await(); + NetSocket socket = client.connect(new ConnectOptions() + .setPort(1234) + .setHost("localhost") + .setSsl(true) + .setSslOptions(new ClientSSLOptions() + .setHostnameVerificationAlgorithm("") + .setTrustAll(true))).await(); + Buffer received = Buffer.buffer(); + Promise done = Promise.promise(); + socket.handler(buff -> { + received.appendBuffer(buff); + if (received.length() == size) { + done.tryComplete(); + } + }); + sent.future().await(); + done.future().await(); + socket.close().await(); + server.close().await(); + assertEquals(content, received.toString()); + } + + @Test + public void testSendFileFromClientAfterTlsUpgrade() throws Exception { + File dir = testFolder.newFolder(); + int size = 64 * 1024; + String content = String.valueOf('a').repeat(size); + File f = setupFile(dir.toString(), "upgraded-client.dat", content); + Buffer received = Buffer.buffer(); + Promise done = Promise.promise(); + server.connectHandler(socket -> { + socket.upgradeToSsl(new ServerSSLOptions().setKeyCertOptions(Cert.SERVER_JKS.get())) + .onSuccess(v -> socket.handler(buff -> { + received.appendBuffer(buff); + if (received.length() == size) { + done.tryComplete(); + } + })); + }); + server.listen(1234, "localhost").await(); + NetSocket socket = client.connect(1234, "localhost").await(); + socket.upgradeToSsl(new ClientSSLOptions() + .setHostnameVerificationAlgorithm("") + .setTrustAll(true)).await(); + socket.sendFile(f.getAbsolutePath()).await(); + done.future().await(); + socket.close().await(); + server.close().await(); + assertEquals(content, received.toString()); + } + @Test public void testSendFileDirectory() throws Exception { File fDir = testFolder.newFolder(); From 661695f2c37d0e5a8e35779ed7597b160f8df269 Mon Sep 17 00:00:00 2001 From: doxlik Date: Sat, 12 Sep 2026 16:21:32 +0400 Subject: [PATCH 3/3] Address review: fix test race, use randomAlphaString and checkpoints Signed-off-by: doxlik --- .../test/java/io/vertx/tests/net/NetTest.java | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java b/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java index fa878656f86..2afc53d9e2b 100755 --- a/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java @@ -2481,16 +2481,16 @@ public void sendFileServerToClient(Checkpoint checkpoint) throws Exception { } @Test - public void testSendFileAfterTlsUpgrade() throws Exception { + public void testSendFileAfterTlsUpgrade(Checkpoint sent, Checkpoint received) throws Exception { File dir = testFolder.newFolder(); int size = 64 * 1024; - String content = String.valueOf('a').repeat(size); + String content = randomAlphaString(size); File f = setupFile(dir.toString(), "upgraded.dat", content); - Promise sent = Promise.promise(); server.connectHandler(socket -> { socket.upgradeToSsl(new ServerSSLOptions().setKeyCertOptions(Cert.SERVER_JKS.get())) - .compose(v -> socket.sendFile(f.getAbsolutePath())) - .onComplete(sent); + .onComplete(onSuccess(v -> socket.handler(ping -> socket + .sendFile(f.getAbsolutePath()) + .onComplete(sent)))); }); server.listen(1234, "localhost").await(); NetSocket socket = client.connect(new ConnectOptions() @@ -2500,37 +2500,36 @@ public void testSendFileAfterTlsUpgrade() throws Exception { .setSslOptions(new ClientSSLOptions() .setHostnameVerificationAlgorithm("") .setTrustAll(true))).await(); - Buffer received = Buffer.buffer(); - Promise done = Promise.promise(); + Buffer body = Buffer.buffer(); socket.handler(buff -> { - received.appendBuffer(buff); - if (received.length() == size) { - done.tryComplete(); + body.appendBuffer(buff); + if (body.length() == size) { + assertEquals(content, body.toString()); + received.succeed(); } }); - sent.future().await(); - done.future().await(); - socket.close().await(); - server.close().await(); - assertEquals(content, received.toString()); + // The server sends the file when it gets this, so that the handler above is set before any data is written + socket.write("ping"); } @Test - public void testSendFileFromClientAfterTlsUpgrade() throws Exception { + public void testSendFileFromClientAfterTlsUpgrade(Checkpoint received) throws Exception { File dir = testFolder.newFolder(); int size = 64 * 1024; - String content = String.valueOf('a').repeat(size); + String content = randomAlphaString(size); File f = setupFile(dir.toString(), "upgraded-client.dat", content); - Buffer received = Buffer.buffer(); - Promise done = Promise.promise(); + Buffer body = Buffer.buffer(); server.connectHandler(socket -> { + // The handler is set before the upgrade, so that no data can be missed when the handshake completes + socket.handler(buff -> { + body.appendBuffer(buff); + if (body.length() == size) { + assertEquals(content, body.toString()); + received.succeed(); + } + }); socket.upgradeToSsl(new ServerSSLOptions().setKeyCertOptions(Cert.SERVER_JKS.get())) - .onSuccess(v -> socket.handler(buff -> { - received.appendBuffer(buff); - if (received.length() == size) { - done.tryComplete(); - } - })); + .onFailure(received::fail); }); server.listen(1234, "localhost").await(); NetSocket socket = client.connect(1234, "localhost").await(); @@ -2538,10 +2537,6 @@ public void testSendFileFromClientAfterTlsUpgrade() throws Exception { .setHostnameVerificationAlgorithm("") .setTrustAll(true)).await(); socket.sendFile(f.getAbsolutePath()).await(); - done.future().await(); - socket.close().await(); - server.close().await(); - assertEquals(content, received.toString()); } @Test