Repository: netty/netty
Issue: Cannot write/flush HTTP/2 with voidPromise

### Expected behavior
Can `write` / `writeAndFlush` frames that may close the stream from a HTTP/2 context

### Actual behavior
`Http2ConnectionHandler#closeStream` tries to `addListener` to the future even if it's void. If void, that will fail.

### Steps to reproduce
`writeAndFlush` a Http2DataFrame to the context with Http2FrameCodec present

### Minimal yet complete reproducer code (or URL to code)
Ran into this using netty [from Clojure](https://github.com/valerauko/iny/blob/7e71ddf649d5b64626139beb784eabf9b5ad88a5/src/iny/http2/handler.clj#L86-L98).

### Netty version
4.1.54

### JVM version (e.g. `java -version`)

### OS version (e.g. `uname -a`)


Issue comments:
@normanmaurer I wrapped the `addListener` call in a try-catch to extract the stacktrace:

```
2020-11-30 12:54:06,441 ERROR [epollEventLoopGroup-3-5] io.netty.util.internal.logging.AbstractInternalLogger | Unexpected exception: 
java.lang.IllegalStateException: void future
	at io.netty.channel.VoidChannelPromise.fail(VoidChannelPromise.java:198)
	at io.netty.channel.VoidChannelPromise.addListener(VoidChannelPromise.java:58)
	at io.netty.channel.VoidChannelPromise.addListener(VoidChannelPromise.java:26)
	at io.netty.handler.codec.http2.Http2ConnectionHandler.closeStream(Http2ConnectionHandler.java:623)
	at io.netty.handler.codec.http2.Http2ConnectionHandler.closeStreamLocal(Http2ConnectionHandler.java:587)
	at io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder$FlowControlledBase.writeComplete(DefaultHttp2ConnectionEncoder.java:622)
	at io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController$FlowState.writeAllocatedBytes(DefaultHttp2RemoteFlowController.java:368)
	at io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController$WritabilityMonitor.write(DefaultHttp2RemoteFlowController.java:547)
	at io.netty.handler.codec.http2.WeightedFairQueueByteDistributor$State.write(WeightedFairQueueByteDistributor.java:604)
	at io.netty.handler.codec.http2.WeightedFairQueueByteDistributor.distribute(WeightedFairQueueByteDistributor.java:292)
	at io.netty.handler.codec.http2.WeightedFairQueueByteDistributor.distributeToChildren(WeightedFairQueueByteDistributor.java:325)
	at io.netty.handler.codec.http2.WeightedFairQueueByteDistributor.distribute(WeightedFairQueueByteDistributor.java:273)
	at io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController$WritabilityMonitor.writePendingBytes(DefaultHttp2RemoteFlowController.java:627)
	at io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController.writePendingBytes(DefaultHttp2RemoteFlowController.java:267)
	at io.netty.handler.codec.http2.Http2ConnectionHandler.flush(Http2ConnectionHandler.java:189)
	at io.netty.channel.AbstractChannelHandlerContext.invokeFlush0(AbstractChannelHandlerContext.java:750)
	at io.netty.channel.AbstractChannelHandlerContext.invokeWriteAndFlush(AbstractChannelHandlerContext.java:765)
	at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:790)
	at io.netty.channel.AbstractChannelHandlerContext.writeAndFlush(AbstractChannelHandlerContext.java:758)
        at (my handler) ...
```

Pull request review comments:
Based on the other tests I wanted to do something like this:
```java
        verify(voidPromise, never()).addListener(any(GenericFutureListener.class));
        verify(ctx.channel(), times(1)).newPromise();
        verify(promise, times(1)).addListener(any(GenericFutureListener.class));
```
I'm not used to this testing suite, so please advise how can I get these checks to work... :pray: 

Patch that resolved the issue:
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java
index 97a7239fb86..d8611c71b8e 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java
@@ -120,6 +120,7 @@ public void remoteSettings(Http2Settings settings) throws Http2Exception {
     @Override
     public ChannelFuture writeData(final ChannelHandlerContext ctx, final int streamId, ByteBuf data, int padding,
             final boolean endOfStream, ChannelPromise promise) {
+        promise = promise.unvoid();
         final Http2Stream stream;
         try {
             stream = requireStream(streamId);


Test patch:
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java
index da9b1221b3b..0eb34f47200 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java
@@ -691,6 +691,14 @@ public ChannelFuture answer(InvocationOnMock invocation) throws Throwable {
         verify(pipeline).fireExceptionCaught(cause);
     }
 
+    @Test
+    public void canCloseStreamWithVoidPromise() throws Exception {
+        handler = newHandler();
+        handler.closeStream(stream, ctx.voidPromise());
+        verify(stream, times(1)).close();
+        verifyNoMoreInteractions(stream);
+    }
+
     @Test
     public void channelReadCompleteTriggersFlush() throws Exception {
         handler = newHandler();