|  | commit a37d3eb349e048b953633027ed011cda8b68c603 | 
|  | Author: George Gensure <werkt0@gmail.com> | 
|  | Date:   Thu Jul 10 09:49:54 2025 -0400 | 
|  |  | 
|  | Guarantee missing stream promise delivery | 
|  |  | 
|  | In observed cases, whether RST_STREAM or another failure from netty or | 
|  | the server, listeners can fail to be notified when a connection yields a | 
|  | null stream for the selected streamId. This causes hangs in clients, | 
|  | despite deadlines, with no obvious resolution. | 
|  |  | 
|  | Tests which relied upon this promise succeeding must now change. | 
|  |  | 
|  | diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java | 
|  | index a5fa0f800..276fa623c 100644 | 
|  | --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java | 
|  | +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java | 
|  | @@ -738,14 +738,19 @@ class NettyClientHandler extends AbstractNettyHandler { | 
|  |  | 
|  | // Attach the client stream to the HTTP/2 stream object as user data. | 
|  | stream.setHttp2Stream(http2Stream); | 
|  | +                promise.setSuccess(); | 
|  | +              } else { | 
|  | +                // Otherwise, the stream has been cancelled and Netty is sending a | 
|  | +                // RST_STREAM frame which causes it to purge pending writes from the | 
|  | +                // flow-controller and delete the http2Stream. The stream listener has already | 
|  | +                // been notified of cancellation so there is nothing to do. | 
|  | +                // | 
|  | +                // This process has been observed to fail in some circumstances, leaving listeners | 
|  | +                // unanswered. Ensure that some exception has been delivered consistent with the | 
|  | +                // implied RST_STREAM result above. | 
|  | +                Status status = Status.INTERNAL.withDescription("unknown stream for connection"); | 
|  | +                promise.setFailure(status.asRuntimeException()); | 
|  | } | 
|  | -              // Otherwise, the stream has been cancelled and Netty is sending a | 
|  | -              // RST_STREAM frame which causes it to purge pending writes from the | 
|  | -              // flow-controller and delete the http2Stream. The stream listener has already | 
|  | -              // been notified of cancellation so there is nothing to do. | 
|  | - | 
|  | -              // Just forward on the success status to the original promise. | 
|  | -              promise.setSuccess(); | 
|  | } else { | 
|  | Throwable cause = future.cause(); | 
|  | if (cause instanceof StreamBufferingEncoder.Http2GoAwayException) { | 
|  | diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java | 
|  | index f8fbeea9b..dd4fcb4ea 100644 | 
|  | --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java | 
|  | +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java | 
|  | @@ -268,7 +268,7 @@ public class NettyClientHandlerTest extends NettyHandlerTestBase<NettyClientHand | 
|  | // Cancel the stream. | 
|  | cancelStream(Status.CANCELLED); | 
|  |  | 
|  | -    assertTrue(createFuture.isSuccess()); | 
|  | +    assertFalse(createFuture.isSuccess()); | 
|  | verify(streamListener).closed(eq(Status.CANCELLED), same(PROCESSED), any(Metadata.class)); | 
|  | } | 
|  |  | 
|  | @@ -311,7 +311,7 @@ public class NettyClientHandlerTest extends NettyHandlerTestBase<NettyClientHand | 
|  | ChannelFuture cancelFuture = cancelStream(Status.CANCELLED); | 
|  | assertTrue(cancelFuture.isSuccess()); | 
|  | assertTrue(createFuture.isDone()); | 
|  | -    assertTrue(createFuture.isSuccess()); | 
|  | +    assertFalse(createFuture.isSuccess()); | 
|  | } | 
|  |  | 
|  | /** |