diff --git a/gradle.properties b/gradle.properties index 291607f50c..e491d5f574 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ group=io.servicetalk version=0.38.0-SNAPSHOT -nettyVersion=4.1.59.Final +nettyVersion=4.1.60.Final tcnativeVersion=2.0.36.Final jsr305Version=3.0.2 diff --git a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/ProtocolCompatibilityTest.java b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/ProtocolCompatibilityTest.java index 38d532eed6..f4dc0031cb 100644 --- a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/ProtocolCompatibilityTest.java +++ b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/ProtocolCompatibilityTest.java @@ -43,8 +43,11 @@ import io.servicetalk.grpc.netty.CompatProto.Compat.ServiceFactory; import io.servicetalk.grpc.netty.CompatProto.RequestContainer.CompatRequest; import io.servicetalk.grpc.netty.CompatProto.ResponseContainer.CompatResponse; +import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpServiceContext; +import io.servicetalk.http.api.StreamingHttpClientFilter; import io.servicetalk.http.api.StreamingHttpRequest; +import io.servicetalk.http.api.StreamingHttpRequester; import io.servicetalk.http.api.StreamingHttpResponse; import io.servicetalk.http.api.StreamingHttpResponseFactory; import io.servicetalk.http.api.StreamingHttpServiceFilter; @@ -100,6 +103,9 @@ import static io.servicetalk.encoding.api.ContentCodings.identity; import static io.servicetalk.grpc.api.GrpcExecutionStrategies.defaultStrategy; import static io.servicetalk.grpc.api.GrpcExecutionStrategies.noOffloadsStrategy; +import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_LENGTH; +import static io.servicetalk.http.api.HttpHeaderNames.TRANSFER_ENCODING; +import static io.servicetalk.http.api.HttpHeaderValues.CHUNKED; import static io.servicetalk.test.resources.DefaultTestCerts.loadServerKey; import static io.servicetalk.test.resources.DefaultTestCerts.loadServerPem; import static io.servicetalk.transport.api.SecurityConfigurator.SslProvider.OPENSSL; @@ -832,6 +838,20 @@ private static CompatClient serviceTalkClient(final SocketAddress serverAddress, builder.secure().disableHostnameVerification().provider(OPENSSL) .trustManager(DefaultTestCerts::loadServerCAPem).commit(); } + // TODO(scott): remove after https://github.com/grpc/grpc-java/issues/7953 is resolved. + builder.appendHttpClientFilter(client -> new StreamingHttpClientFilter(client) { + @Override + protected Single request(final StreamingHttpRequester delegate, + final HttpExecutionStrategy strategy, + final StreamingHttpRequest request) { + return Single.defer(() -> { + // Force chunked transfer encoding as a workaround for grpc-java bug. + request.headers().remove(CONTENT_LENGTH); + request.headers().set(TRANSFER_ENCODING, CHUNKED); + return delegate.request(strategy, request).subscribeShareContext(); + }); + } + }); List codings = serviceTalkCodingsFor(compression); return builder.build(new Compat.ClientFactory().supportedMessageCodings(codings)); } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/AbstractH2DuplexHandler.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/AbstractH2DuplexHandler.java index e7bb8c54f5..46ec0bb11d 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/AbstractH2DuplexHandler.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/AbstractH2DuplexHandler.java @@ -32,37 +32,21 @@ import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2ResetFrame; import io.netty.util.ReferenceCountUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import static io.netty.buffer.Unpooled.EMPTY_BUFFER; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; import static io.servicetalk.buffer.netty.BufferUtils.toByteBuf; import static io.servicetalk.http.netty.H2ToStH1Utils.h1HeadersToH2Headers; import static io.servicetalk.http.netty.Http2Exception.newStreamResetException; import static io.servicetalk.http.netty.HttpObjectEncoder.encodeAndRetain; import static io.servicetalk.transport.netty.internal.ChannelCloseUtils.channelError; -import static java.lang.Boolean.getBoolean; -import static java.lang.Math.addExact; abstract class AbstractH2DuplexHandler extends ChannelDuplexHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractH2DuplexHandler.class); - /** - * Temporary opt-out of - * Malformed Requests and Responses checks. - * This is only meant to ease interoperability until violating implementations are fixed. - * Will be removed in future release! - */ - private static final boolean ALLOW_INVALID_CONTENT_LENGTH = - getBoolean("io.servicetalk.http2.allowInvalidContentLength"); final BufferAllocator allocator; final HttpHeadersFactory headersFactory; final CloseHandler closeHandler; private final StreamObserver observer; - private long contentLength = Long.MIN_VALUE; - private long seenContentLength; AbstractH2DuplexHandler(BufferAllocator allocator, HttpHeadersFactory headersFactory, CloseHandler closeHandler, StreamObserver observer) { @@ -101,7 +85,6 @@ final void readDataFrame(ChannelHandlerContext ctx, Object msg) { Http2DataFrame dataFrame = (Http2DataFrame) msg; final int readableBytes = dataFrame.content().readableBytes(); if (readableBytes > 0) { - updateSeenContentLength(readableBytes); // Copy to unpooled memory before passing to the user Buffer data = allocator.newBuffer(readableBytes); ByteBuf nettyData = toByteBuf(data); @@ -112,7 +95,6 @@ final void readDataFrame(ChannelHandlerContext ctx, Object msg) { toRelease = release(dataFrame); } if (dataFrame.isEndStream()) { - validateContentLengthMatch(); ctx.fireChannelRead(headersFactory.newEmptyTrailers()); closeHandler.protocolPayloadEndInbound(ctx); } @@ -143,39 +125,4 @@ public void channelInactive(final ChannelHandlerContext ctx) { } ctx.fireChannelInactive(); } - - final long contentLength(final Http2Headers headers) { - if (contentLength == Long.MIN_VALUE) { - contentLength = HeaderUtils.contentLength(headers.valueIterator(CONTENT_LENGTH), headers::getAll); - } - return contentLength; - } - - final void validateContentLengthMatch() { - if (contentLength >= 0 && seenContentLength != contentLength) { - handleUnexpectedContentLength(); - } - } - - private void updateSeenContentLength(final int readableBytes) { - assert readableBytes >= 0; - if (contentLength < 0) { - return; - } - seenContentLength = addExact(seenContentLength, readableBytes); - if (seenContentLength > contentLength) { - handleUnexpectedContentLength(); - } - } - - final void handleUnexpectedContentLength() { - final String msg = "Expected content-length " + contentLength + " not equal to the actual length " + - seenContentLength + - ". Malformed request/response according to https://tools.ietf.org/html/rfc7540#section-8.1.2.6."; - if (ALLOW_INVALID_CONTENT_LENGTH) { - LOGGER.info(msg); - } else { - throw new IllegalArgumentException(msg); - } - } } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ToStH1ClientDuplexHandler.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ToStH1ClientDuplexHandler.java index a3a9ddc78d..6a0b070071 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ToStH1ClientDuplexHandler.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ToStH1ClientDuplexHandler.java @@ -43,12 +43,12 @@ import static io.servicetalk.http.api.HttpHeaderValues.ZERO; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_2_0; import static io.servicetalk.http.api.HttpRequestMethod.CONNECT; -import static io.servicetalk.http.api.HttpRequestMethod.HEAD; import static io.servicetalk.http.api.HttpResponseMetaDataFactory.newResponseMetaData; import static io.servicetalk.http.api.HttpResponseStatus.StatusClass.INFORMATIONAL_1XX; import static io.servicetalk.http.netty.H2ToStH1Utils.h1HeadersToH2Headers; import static io.servicetalk.http.netty.H2ToStH1Utils.h2HeadersSanitizeForH1; import static io.servicetalk.http.netty.HeaderUtils.canAddResponseTransferEncodingProtocol; +import static io.servicetalk.http.netty.HeaderUtils.contentLength; import static io.servicetalk.http.netty.HeaderUtils.shouldAddZeroContentLength; final class H2ToStH1ClientDuplexHandler extends AbstractH2DuplexHandler { @@ -136,14 +136,6 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { if (httpStatus != null) { fireFullResponse(ctx, h2Headers, httpStatus); } else { - if (!HEAD.equals(method)) { - // https://tools.ietf.org/html/rfc7230#section-3.3 - // Responses to the HEAD request method (Section 4.3.2 of [RFC7231]) never include a message - // body because the associated response header fields (e.g., Transfer-Encoding, Content-Length, - // etc.), if present, indicate only what their values would have been if the request method had - // been GET (Section 4.3.1 of [RFC7231]). - validateContentLengthMatch(); - } ctx.fireChannelRead(h2HeadersToH1HeadersClient(h2Headers, null, false)); } closeHandler.protocolPayloadEndInbound(ctx); @@ -175,7 +167,8 @@ private NettyH2HeadersToHttpHeaders h2HeadersToH1HeadersClient(Http2Headers h2He h2HeadersSanitizeForH1(h2Headers); if (httpStatus != null) { final int statusCode = httpStatus.code(); - final long contentLength = contentLength(h2Headers); + final long contentLength = contentLength(h2Headers.valueIterator(HttpHeaderNames.CONTENT_LENGTH), + h2Headers::getAll); if (contentLength < 0) { if (fullResponse) { if (shouldAddZeroContentLength(httpStatus.code(), method)) { @@ -188,8 +181,6 @@ private NettyH2HeadersToHttpHeaders h2HeadersToH1HeadersClient(Http2Headers h2He throw new IllegalArgumentException("content-length (" + contentLength + ") header is not expected for status code " + statusCode + " in response to " + method.name() + " request"); - } else if (fullResponse && contentLength > 0 && !HEAD.equals(method)) { - handleUnexpectedContentLength(); } } return new NettyH2HeadersToHttpHeaders(h2Headers, headersFactory.validateCookies()); diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ToStH1ServerDuplexHandler.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ToStH1ServerDuplexHandler.java index 4982c1437b..22669ab5df 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ToStH1ServerDuplexHandler.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ToStH1ServerDuplexHandler.java @@ -26,6 +26,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; import io.netty.handler.codec.http2.Http2DataFrame; import io.netty.handler.codec.http2.Http2Headers; @@ -47,6 +48,7 @@ import static io.servicetalk.http.netty.H2ToStH1Utils.h1HeadersToH2Headers; import static io.servicetalk.http.netty.H2ToStH1Utils.h2HeadersSanitizeForH1; import static io.servicetalk.http.netty.HeaderUtils.clientMaySendPayloadBodyFor; +import static io.servicetalk.http.netty.HeaderUtils.contentLength; final class H2ToStH1ServerDuplexHandler extends AbstractH2DuplexHandler { private boolean readHeaders; @@ -100,7 +102,6 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { if (httpMethod != null) { fireFullRequest(ctx, h2Headers, httpMethod, path); } else { - validateContentLengthMatch(); ctx.fireChannelRead(h2TrailersToH1TrailersServer(h2Headers)); } closeHandler.protocolPayloadEndInbound(ctx); @@ -135,7 +136,8 @@ private NettyH2HeadersToHttpHeaders h2HeadersToH1HeadersServer(Http2Headers h2He h2Headers.remove(Http2Headers.PseudoHeaderName.SCHEME.value()); h2HeadersSanitizeForH1(h2Headers); if (httpMethod != null) { - final long contentLength = contentLength(h2Headers); + final long contentLength = contentLength(h2Headers.valueIterator(HttpHeaderNames.CONTENT_LENGTH), + h2Headers::getAll); if (clientMaySendPayloadBodyFor(httpMethod)) { if (contentLength < 0) { if (fullRequest) { @@ -143,8 +145,6 @@ private NettyH2HeadersToHttpHeaders h2HeadersToH1HeadersServer(Http2Headers h2He } else { h2Headers.add(TRANSFER_ENCODING, CHUNKED); } - } else if (fullRequest && contentLength > 0) { - handleUnexpectedContentLength(); } } else if (contentLength >= 0) { throw new IllegalArgumentException("content-length (" + contentLength + diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AbstractH2DuplexHandlerTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AbstractH2DuplexHandlerTest.java index 3049f96f57..cbc0218b2c 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AbstractH2DuplexHandlerTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AbstractH2DuplexHandlerTest.java @@ -56,7 +56,6 @@ import static io.servicetalk.transport.netty.internal.CloseHandler.forNonPipelined; import static java.lang.String.valueOf; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -303,18 +302,6 @@ private void withContentLength(boolean addTrailers) { assertThat(channel.inboundMessages(), is(empty())); } - @Test - public void singleHeadersFrameWithContentLength() { - variant.writeOutbound(channel); - - Http2Headers headers = variant.setHeaders(new DefaultHttp2Headers()); - headers.setInt(CONTENT_LENGTH, 1); - - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> channel.writeInbound(new DefaultHttp2HeadersFrame(headers, true))); - assertThat(e.getMessage(), containsString("not equal to the actual length")); - } - @Test public void singleHeadersFrameWithZeroContentLength() { variant.writeOutbound(channel); @@ -330,54 +317,4 @@ public void singleHeadersFrameWithZeroContentLength() { assertThat(trailers.isEmpty(), is(true)); assertThat(channel.inboundMessages(), is(empty())); } - - @Test - public void lessThanActual() { - invalidContentLength(3, "hello", false); - } - - @Test - public void lessThanActualWithTrailers() { - invalidContentLength(3, "hello", true); - } - - @Test - public void notEqualToActualLength() { - invalidContentLength(10, "hello", false); - } - - @Test - public void notEqualToActualLengthWithTrailers() { - invalidContentLength(10, "hello", true); - } - - private void invalidContentLength(int contentLength, String content, boolean addTrailers) { - variant.writeOutbound(channel); - - Http2Headers headers = variant.setHeaders(new DefaultHttp2Headers()); - headers.setInt(CONTENT_LENGTH, contentLength); - channel.writeInbound(new DefaultHttp2HeadersFrame(headers)); - - HttpMetaData metaData = channel.readInbound(); - assertThat(metaData.headers().get(CONTENT_LENGTH), contentEqualTo(valueOf(contentLength))); - - final IllegalArgumentException e; - if (addTrailers) { - if (contentLength < content.length()) { - e = assertThrows(IllegalArgumentException.class, () -> channel.writeInbound(new DefaultHttp2DataFrame( - writeAscii(UnpooledByteBufAllocator.DEFAULT, content)))); - } else { - channel.writeInbound(new DefaultHttp2DataFrame(writeAscii(UnpooledByteBufAllocator.DEFAULT, content))); - Buffer buffer = channel.readInbound(); - assertThat(buffer, is(equalTo(DEFAULT_ALLOCATOR.fromAscii(content)))); - - e = assertThrows(IllegalArgumentException.class, () -> channel.writeInbound( - new DefaultHttp2HeadersFrame(new DefaultHttp2Headers().set("trailer", "value"), true))); - } - } else { - e = assertThrows(IllegalArgumentException.class, () -> channel.writeInbound(new DefaultHttp2DataFrame( - writeAscii(UnpooledByteBufAllocator.DEFAULT, content), true))); - } - assertThat(e.getMessage(), containsString("not equal to the actual length")); - } } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2PriorKnowledgeFeatureParityTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2PriorKnowledgeFeatureParityTest.java index 88292ddeaa..bb723ac999 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2PriorKnowledgeFeatureParityTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2PriorKnowledgeFeatureParityTest.java @@ -66,6 +66,7 @@ import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; +import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http2.DefaultHttp2DataFrame; import io.netty.handler.codec.http2.DefaultHttp2Headers; @@ -82,6 +83,7 @@ import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; +import org.junit.function.ThrowingRunnable; import org.junit.rules.Timeout; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -93,6 +95,7 @@ import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.InetSocketAddress; +import java.nio.channels.ClosedChannelException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; @@ -124,6 +127,7 @@ import static io.servicetalk.http.api.HttpHeaderNames.COOKIE; import static io.servicetalk.http.api.HttpHeaderNames.EXPECT; import static io.servicetalk.http.api.HttpHeaderNames.SET_COOKIE; +import static io.servicetalk.http.api.HttpHeaderNames.TRANSFER_ENCODING; import static io.servicetalk.http.api.HttpHeaderValues.CONTINUE; import static io.servicetalk.http.api.HttpRequestMethod.GET; import static io.servicetalk.http.api.HttpRequestMethod.POST; @@ -147,9 +151,11 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.function.UnaryOperator.identity; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isEmptyString; import static org.hamcrest.Matchers.notNullValue; @@ -159,6 +165,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -435,6 +442,126 @@ public void serverHeaderCookieRemovalAndIteration() throws Exception { } } + @Test + public void clientSendsLargerContentLength() throws Exception { + assumeTrue(h2PriorKnowledge); // http/1.x will timeout waiting for more payload. + clientSendsInvalidContentLength(1, false); + } + + @Test + public void clientSendsLargerContentLengthTrailers() throws Exception { + assumeTrue(h2PriorKnowledge); // http/1.x will timeout waiting for more payload. + clientSendsInvalidContentLength(1, true); + } + + @Test + public void clientSendsSmallerContentLength() throws Exception { + clientSendsInvalidContentLength(-1, false); + } + + @Test + public void clientSendsSmallerContentLengthTrailers() throws Exception { + clientSendsInvalidContentLength(-1, true); + } + + private void clientSendsInvalidContentLength(int contentLengthAdder, boolean addTrailers) throws Exception { + InetSocketAddress serverAddress = bindHttpEchoServer(); + try (BlockingHttpClient client = forSingleAddress(HostAndPort.of(serverAddress)) + .protocols(h2PriorKnowledge ? h2Default() : h1Default()) + .executionStrategy(clientExecutionStrategy) + .appendClientFilter(client1 -> new StreamingHttpClientFilter(client1) { + @Override + protected Single request(final StreamingHttpRequester delegate, + final HttpExecutionStrategy strategy, + final StreamingHttpRequest request) { + return request.toRequest().map(req -> { + req.headers().remove(TRANSFER_ENCODING); + req.headers().set(CONTENT_LENGTH, + String.valueOf(req.payloadBody().readableBytes() + contentLengthAdder)); + return req.toStreamingRequest(); + }).flatMap(req -> delegate.request(strategy, req)); + } + }).buildBlocking()) { + HttpRequest request = client.get("/").payloadBody("a", textSerializer()); + if (addTrailers) { + request.trailers().set("mytrailer", "myvalue"); + } + if (h2PriorKnowledge) { + assertThrows(Http2Exception.H2StreamResetException.class, () -> client.request(request)); + } else { + ReservedBlockingHttpConnection reservedConn = client.reserveConnection(request); + try { + reservedConn.request(request); + assertThrows(ClosedChannelException.class, () -> + reservedConn.request(client.get("/").payloadBody("a", textSerializer()))); + } finally { + assertThrows(IllegalStateException.class, reservedConn::release); + } + } + } + } + + @Test + public void serverSendsLargerContentLength() throws Exception { + assumeTrue(h2PriorKnowledge); // http/1.x will timeout waiting for more payload. + serverSendsInvalidContentLength(1, false); + } + + @Test + public void serverSendsLargerContentLengthTrailers() throws Exception { + assumeTrue(h2PriorKnowledge); // http/1.x will timeout waiting for more payload. + serverSendsInvalidContentLength(1, true); + } + + @Test + public void serverSendsSmallerContentLength() throws Exception { + serverSendsInvalidContentLength(-1, false); + } + + @Test + public void serverSendsSmallerContentLengthTrailers() throws Exception { + serverSendsInvalidContentLength(-1, true); + } + + private void serverSendsInvalidContentLength(int contentLengthAdder, boolean addTrailers) throws Exception { + InetSocketAddress serverAddress = bindHttpEchoServer(service -> new StreamingHttpServiceFilter(service) { + @Override + public Single handle(final HttpServiceContext ctx, + final StreamingHttpRequest request, + final StreamingHttpResponseFactory responseFactory) { + return delegate().handle(ctx, request, responseFactory).flatMap(resp -> + resp.toResponse().map(aggResp -> { + aggResp.headers().remove(TRANSFER_ENCODING); + aggResp.headers().set(CONTENT_LENGTH, + String.valueOf(aggResp.payloadBody().readableBytes() + contentLengthAdder)); + return aggResp.toStreamingResponse(); + })); + } + }); + try (BlockingHttpClient client = forSingleAddress(HostAndPort.of(serverAddress)) + .protocols(h2PriorKnowledge ? h2Default() : h1Default()) + .executionStrategy(clientExecutionStrategy) + .buildBlocking()) { + HttpRequest request = client.get("/").payloadBody("a", textSerializer()); + if (addTrailers) { + request.trailers().set("mytrailer", "myvalue"); + } + if (h2PriorKnowledge) { + assertThat(invokeThrowableRunnable(() -> client.request(request)), + either(instanceOf(Http2Exception.class)).or(instanceOf(ClosedChannelException.class))); + } else { + ReservedBlockingHttpConnection reservedConn = client.reserveConnection(request); + try { + reservedConn.request(request); + assertThrows(DecoderException.class, () -> + reservedConn.request(client.get("/").payloadBody("a", textSerializer()))); + } finally { + invokeThrowableRunnable(reservedConn::release); + } + } + } + } + @Test public void clientHeaderCookieRemovalAndIteration() throws Exception { InetSocketAddress serverAddress = bindHttpEchoServer(); @@ -1432,4 +1559,14 @@ protected HttpHeaders payloadComplete(final HttpHeaders trailers) { return trailers; } } + + @Nullable + private static Throwable invokeThrowableRunnable(ThrowingRunnable runnable) { + try { + runnable.run(); + } catch (Throwable cause) { + return cause; + } + return null; + } }