Repository: netty/netty
Issue: exception when  close channel before SSL 

### Expected behavior
close channel no exception
### Actual behavior
2022/01/13 17:56:07.897 [] [reactor-http-epoll-2] [WARN] i.n.h.s.ApplicationProtocolNegotiationHandler - [id: 0xa03cf599, L:/189.11.24.119:8443 ! R:/189.11.24.38:39277] Failed to select the application-level protocol:
java.nio.channels.ClosedChannelException: null
	at io.netty.handler.ssl.SslHandler.channelInactive(SslHandler.java:1063) [netty-handler-4.1.72.Final.jar!/:4.1.72.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:262) [netty-transport-4.1.72.Final.jar!/:4.1.72.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:248) [netty-transport-4.1.72.Final.jar!/:4.1.72.Final]
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelInactive(AbstractChannelHandlerContext.java:241) [netty-transport-4.1.72.Final.jar!/:4.1.72.Final]
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelInactive(DefaultChannelPipeline.java:1405) [netty-transport-4.1.72.Final.jar!/:4.1.72.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:262) [netty-transport-4.1.72.Final.jar!/:4.1.72.Final]
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:248) [netty-transport-4.1.72.Final.jar!/:4.1.72.Final]
	at io.netty.channel.DefaultChannelPipeline.fireChannelInactive(DefaultChannelPipeline.java:901) [netty-transport-4.1.72.Final.jar!/:4.1.72.Final]
	at io.netty.channel.AbstractChannel$AbstractUnsafe$8.run(AbstractChannel.java:831) 
### Steps to reproduce
server is 189.11.24.119 8443
client is 189.11.24.38 
The tcpdump packet capture result is as follows:
![image](https://user-images.githubusercontent.com/13596975/149335475-2972a124-bafa-482a-90f1-4cc94f4a8710.png)

### Minimal yet complete reproducer code (or URL to code)
NA
### Netty version
4.1.72.Final
### JVM version (e.g. `java -version`)
openjdk 11.0.8 
### OS version (e.g. `uname -a`)
linux x86

Issue comments:
Related to https://github.com/spring-cloud/spring-cloud-gateway/issues/2493

Pull request review comments:
I think we still need to ensure we only create a `SSLHandshakeException` if we are really still handshaking.
```suggestion
        final Exception exception;
        if (handshakePromise.isDone()) {
            exception = new ClosedChannelException();
        } else {
            // Closed before the handshake was done.
            exception = new SSLHandshakeException("Connection closed during handshake");
            exception.initCause(new ClosedChannelException()); 
        }
```
There is a possibility that the channel will become inactive even before it was able to read anything and start the handshake. Consider `!isStateSet(STATE_HANDSHAKE_STARTED) || handshakePromise.isDone()` 
It doesn't provide much value and produces too much logging output if we generate stack-trace twice from the same method. Consider making either `ClosedChannelException` or `SSLHandshakeException` stack-less in this case.
In the previous round of feedback the logic was opposite. The above condition was for `ClosedChannelException`. Since now we changed the logic, we have to flip this condition as well. Consider: `isStateSet(STATE_HANDSHAKE_STARTED) && !handshakePromise.isDone()`.
"before" doesn't help to understand if it was "before handshake started" or "after it started but before it complete". Consider clarifying that the handshake started, but didn't complete
Since it's a subtype of `SSLHandshakeException` with the same meaning, consider making it pkg-private.
Yeah, sorry. My bad. ;/
```suggestion
                    new StacklessSSLHandshakeException("Connection closed while SSL/TLS handshake was in progress"));
```

Patch that resolved the issue:
diff --git a/handler/src/main/java/io/netty/handler/ssl/SslHandler.java b/handler/src/main/java/io/netty/handler/ssl/SslHandler.java
index d59b504c709..83ce18e747f 100644
--- a/handler/src/main/java/io/netty/handler/ssl/SslHandler.java
+++ b/handler/src/main/java/io/netty/handler/ssl/SslHandler.java
@@ -47,6 +47,7 @@
 import io.netty.util.concurrent.PromiseNotifier;
 import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.ThrowableUtil;
 import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -1062,7 +1063,15 @@ private SSLEngineResult wrap(ByteBufAllocator alloc, SSLEngine engine, ByteBuf i
     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
         boolean handshakeFailed = handshakePromise.cause() != null;
 
+        // Channel closed, we will generate 'ClosedChannelException' now.
         ClosedChannelException exception = new ClosedChannelException();
+
+        // Add a supressed exception if the handshake was not completed yet.
+        if (isStateSet(STATE_HANDSHAKE_STARTED) && !handshakePromise.isDone()) {
+            ThrowableUtil.addSuppressed(exception,
+                    new StacklessSSLHandshakeException("Connection closed while SSL/TLS handshake was in progress"));
+        }
+
         // Make sure to release SSLEngine,
         // and notify the handshake future if the connection has been closed during handshake.
         setHandshakeFailure(ctx, exception, !isStateSet(STATE_OUTBOUND_CLOSED), isStateSet(STATE_HANDSHAKE_STARTED),
diff --git a/handler/src/main/java/io/netty/handler/ssl/StacklessSSLHandshakeException.java b/handler/src/main/java/io/netty/handler/ssl/StacklessSSLHandshakeException.java
new file mode 100644
index 00000000000..3c315892fea
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/StacklessSSLHandshakeException.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.ssl;
+
+import javax.net.ssl.SSLHandshakeException;
+
+/**
+ * A {@link SSLHandshakeException} that does not fill in the stack trace.
+ */
+final class StacklessSSLHandshakeException extends SSLHandshakeException {
+
+    private static final long serialVersionUID = -1244781947804415549L;
+
+    /**
+     * Constructs an exception reporting an error found by
+     * an SSL subsystem during handshaking.
+     *
+     * @param reason describes the problem.
+     */
+    StacklessSSLHandshakeException(String reason) {
+        super(reason);
+    }
+
+    @Override
+    public Throwable fillInStackTrace() {
+        // This is a performance optimization to not fill in the
+        // stack trace as this is a stackless exception.
+        return this;
+    }
+}


Test patch:
diff --git a/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java b/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java
index 70aef23b602..bd8e0ac79e5 100644
--- a/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java
@@ -568,8 +568,8 @@ public void testCloseFutureNotified() throws Exception {
 
         assertFalse(ch.finishAndReleaseAll());
 
-        assertTrue(handler.handshakeFuture().cause() instanceof ClosedChannelException);
-        assertTrue(handler.sslCloseFuture().cause() instanceof ClosedChannelException);
+        assertThat(handler.handshakeFuture().cause(), instanceOf(ClosedChannelException.class));
+        assertThat(handler.sslCloseFuture().cause(), instanceOf(ClosedChannelException.class));
     }
 
     @Test
@@ -590,11 +590,11 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
 
         SslCompletionEvent evt = events.take();
         assertTrue(evt instanceof SslHandshakeCompletionEvent);
-        assertTrue(evt.cause() instanceof ClosedChannelException);
+        assertThat(evt.cause(), instanceOf(ClosedChannelException.class));
 
         evt = events.take();
         assertTrue(evt instanceof SslCloseCompletionEvent);
-        assertTrue(evt.cause() instanceof ClosedChannelException);
+        assertThat(evt.cause(), instanceOf(ClosedChannelException.class));
         assertTrue(events.isEmpty());
     }