diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java index a0c8b56e74..06c1351818 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java @@ -752,7 +752,9 @@ protected void onInboundNext(ChannelHandlerContext ctx, Object msg) { log.debug(format(channel(), "Received last HTTP packet")); } if (msg != LastHttpContent.EMPTY_LAST_CONTENT) { - if (redirecting != null) { + // When there is HTTP/2 response with INBOUND HEADERS(endStream=false) followed by INBOUND DATA(endStream=true length=0), + // Netty sends LastHttpContent with empty buffer instead of EMPTY_LAST_CONTENT + if (redirecting != null || ((LastHttpContent) msg).content().readableBytes() == 0) { ReferenceCountUtil.release(msg); } else { diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/Http2Tests.java b/reactor-netty-http/src/test/java/reactor/netty/http/Http2Tests.java index 7d864a199d..dbac3bcd2f 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/Http2Tests.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/Http2Tests.java @@ -16,6 +16,7 @@ package reactor.netty.http; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2FrameCodec; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -44,6 +45,7 @@ import reactor.util.annotation.Nullable; import reactor.util.function.Tuple2; +import java.nio.charset.Charset; import java.security.cert.CertificateException; import java.time.Duration; import java.util.List; @@ -695,4 +697,40 @@ private void doTestMaxStreams(HttpServer server, HttpClient client) { .expectComplete() .verify(Duration.ofSeconds(5)); } + + @ParameterizedTest + @MethodSource("h2cCompatibleCombinations") + void testEmptyDataFrameH2C(HttpProtocol[] serverProtocols, HttpProtocol[] clientProtocols) { + doTestEmptyDataFrame(createServer().protocol(serverProtocols), + createClient(() -> disposableServer.address()).protocol(clientProtocols)); + } + + @ParameterizedTest + @MethodSource("h2CompatibleCombinations") + void testEmptyDataFrameH2(HttpProtocol[] serverProtocols, HttpProtocol[] clientProtocols) { + Http2SslContextSpec serverCtx = Http2SslContextSpec.forServer(ssc.certificate(), ssc.privateKey()); + Http2SslContextSpec clientCtx = + Http2SslContextSpec.forClient() + .configure(builder -> builder.trustManager(InsecureTrustManagerFactory.INSTANCE)); + + doTestEmptyDataFrame(createServer().protocol(serverProtocols).secure(spec -> spec.sslContext(serverCtx)), + createClient(() -> disposableServer.address()).protocol(clientProtocols).secure(spec -> spec.sslContext(clientCtx))); + } + + private void doTestEmptyDataFrame(HttpServer server, HttpClient client) { + disposableServer = + // Intentionality sends Flux with empty strings as we want to have + // OUTBOUND HEADERS(endStream=false) followed by OUTBOUND DATA(endStream=true length=0) + server.handle((req, res) -> res.sendString(Flux.just("", ""))) + .bindNow(); + + String expectation = "EMPTY"; + client.get() + .uri("/") + .response((res, bytes) -> bytes.defaultIfEmpty(Unpooled.wrappedBuffer(expectation.getBytes(Charset.defaultCharset())))) + .as(StepVerifier::create) + .expectNextMatches(buf -> expectation.equals(buf.toString(Charset.defaultCharset()))) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } }