From 9ce84dcb21168c45fe6714d3673c9253f35e5cd2 Mon Sep 17 00:00:00 2001 From: Moses Nakamura Date: Tue, 3 May 2016 23:51:45 -0700 Subject: [PATCH] Http2 to Http1.1 converter on new Http2 server API Motivation: http/2 and http/1.1 have similar protocols, and it's useful to be able to implement a single server against a single interface. There's an injection from http/1.1 messages to http/2 ones, so it makes sense to make folks program against http/1.1 and upgrade them under the hood. Modifications: added a MessageToMessageCodec which turns every kind of Http2StreamFrame domain object into an HttpObject domain object, and then back again on the way out. This one is specialized for servers, but it should be straightforward to make a symmetric one for clients, or else extend this one. Result: fixes #5199, and it's now simple to make your Http2MultiplexCodec speak Http1.1 --- .../codec/http2/Http2ServerDowngrader.java | 126 +++++++ .../codec/http2/HttpConversionUtil.java | 34 +- .../http2/InboundHttp2ToHttpAdapter.java | 2 +- .../http2/Http2ServerDowngraderTest.java | 317 ++++++++++++++++++ 4 files changed, 477 insertions(+), 2 deletions(-) create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerDowngrader.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerDowngraderTest.java diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerDowngrader.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerDowngrader.java new file mode 100644 index 0000000000..189e2cd52c --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerDowngrader.java @@ -0,0 +1,126 @@ +/* + * Copyright 2016 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: + * + * http://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.codec.http2; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageCodec; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.ReferenceCountUtil; + +import java.util.List; + +/** + * This is a server-side adapter so that an http2 codec can be downgraded to + * appear as if it's speaking http/1.1. + * + * In particular, this handler converts from {@link Http2StreamFrame} to {@link + * HttpObject}, and back. For simplicity, it converts to chunked encoding + * unless the entire stream is a single header. + */ +public class Http2ServerDowngrader extends MessageToMessageCodec { + + private final boolean validateHeaders; + + public Http2ServerDowngrader(boolean validateHeaders) { + this.validateHeaders = validateHeaders; + } + + public Http2ServerDowngrader() { + this(true); + } + + @Override + public boolean acceptInboundMessage(Object msg) throws Exception { + return (msg instanceof Http2HeadersFrame) || (msg instanceof Http2DataFrame); + } + + @Override + protected void decode(ChannelHandlerContext ctx, Http2StreamFrame frame, List out) throws Exception { + if (frame instanceof Http2HeadersFrame) { + int id = 0; // not really the id + Http2HeadersFrame headersFrame = (Http2HeadersFrame) frame; + Http2Headers headers = headersFrame.headers(); + + if (headersFrame.isEndStream()) { + if (headers.method() == null) { + LastHttpContent last = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders); + HttpConversionUtil.addHttp2ToHttpHeaders(id, headers, last.trailingHeaders(), + HttpVersion.HTTP_1_1, true, true); + out.add(last); + } else { + FullHttpRequest full = HttpConversionUtil.toFullHttpRequest(id, headers, ctx.alloc(), + validateHeaders); + out.add(full); + } + } else { + out.add(HttpConversionUtil.toHttpRequest(id, headersFrame.headers(), validateHeaders)); + } + + } else if (frame instanceof Http2DataFrame) { + Http2DataFrame dataFrame = (Http2DataFrame) frame; + if (dataFrame.isEndStream()) { + out.add(new DefaultLastHttpContent(dataFrame.content(), validateHeaders)); + } else { + out.add(new DefaultHttpContent(dataFrame.content())); + } + } + ReferenceCountUtil.retain(frame); + } + + private void encodeLastContent(LastHttpContent last, List out) { + boolean needFiller = !(last instanceof FullHttpResponse) && last.trailingHeaders().isEmpty(); + if (last.content().isReadable() || needFiller) { + out.add(new DefaultHttp2DataFrame(last.content(), last.trailingHeaders().isEmpty())); + } + if (!last.trailingHeaders().isEmpty()) { + Http2Headers headers = HttpConversionUtil.toHttp2Headers(last.trailingHeaders(), validateHeaders); + out.add(new DefaultHttp2HeadersFrame(headers, true)); + } + } + + @Override + protected void encode(ChannelHandlerContext ctx, HttpObject obj, List out) throws Exception { + if (obj instanceof HttpResponse) { + Http2Headers headers = HttpConversionUtil.toHttp2Headers((HttpResponse) obj, validateHeaders); + boolean noMoreFrames = false; + if (obj instanceof FullHttpResponse) { + FullHttpResponse full = (FullHttpResponse) obj; + noMoreFrames = !full.content().isReadable() && full.trailingHeaders().isEmpty(); + } + + out.add(new DefaultHttp2HeadersFrame(headers, noMoreFrames)); + } + + if (obj instanceof LastHttpContent) { + LastHttpContent last = (LastHttpContent) obj; + encodeLastContent(last, out); + } else if (obj instanceof HttpContent) { + HttpContent cont = (HttpContent) obj; + out.add(new DefaultHttp2DataFrame(cont.content(), false)); + } + ReferenceCountUtil.retain(obj); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java index 1d24bc069f..056aafc92e 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBufAllocator; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; @@ -230,7 +231,7 @@ public final class HttpConversionUtil { * @return A new request object which represents headers/data * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)} */ - public static FullHttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, ByteBufAllocator alloc, + public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers, ByteBufAllocator alloc, boolean validateHttpHeaders) throws Http2Exception { // HTTP/2 does not define a way to carry the version identifier that is included in the HTTP/1.1 request line. @@ -252,6 +253,37 @@ public final class HttpConversionUtil { return msg; } + /** + * Create a new object to contain the request data. + * + * @param streamId The stream associated with the request + * @param http2Headers The initial set of HTTP/2 headers to create the request with + * @param validateHttpHeaders
    + *
  • {@code true} to validate HTTP headers in the http-codec
  • + *
  • {@code false} not to validate HTTP headers in the http-codec
  • + *
+ * @return A new request object which represents headers for a chunked request + * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)} + */ + public static HttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders) + throws Http2Exception { + // HTTP/2 does not define a way to carry the version identifier that is included in the HTTP/1.1 request line. + final CharSequence method = checkNotNull(http2Headers.method(), + "method header cannot be null in conversion to HTTP/1.x"); + final CharSequence path = checkNotNull(http2Headers.path(), + "path header cannot be null in conversion to HTTP/1.x"); + HttpRequest msg = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method.toString()), + path.toString(), validateHttpHeaders); + try { + addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, true); + } catch (Http2Exception e) { + throw e; + } catch (Throwable t) { + throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error"); + } + return msg; + } + /** * Translate and add HTTP/2 headers to HTTP/1.x headers. * diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java index c67ffaecb0..2fec4a5c05 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java @@ -152,7 +152,7 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter { protected FullHttpMessage newMessage(Http2Stream stream, Http2Headers headers, boolean validateHttpHeaders, ByteBufAllocator alloc) throws Http2Exception { - return connection.isServer() ? HttpConversionUtil.toHttpRequest(stream.id(), headers, alloc, + return connection.isServer() ? HttpConversionUtil.toFullHttpRequest(stream.id(), headers, alloc, validateHttpHeaders) : HttpConversionUtil.toHttpResponse(stream.id(), headers, alloc, validateHttpHeaders); } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerDowngraderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerDowngraderTest.java new file mode 100644 index 0000000000..e7ca6702e7 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerDowngraderTest.java @@ -0,0 +1,317 @@ +/* + * Copyright 2016 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: + * + * http://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.codec.http2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.CharsetUtil; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +public class Http2ServerDowngraderTest { + + @Test + public void testUpgradeEmptyFullResponse() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + assertTrue(ch.writeOutbound(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK))); + + Http2HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("200")); + assertTrue(headersFrame.isEndStream()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeNonEmptyFullResponse() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + assertTrue(ch.writeOutbound(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, hello))); + + Http2HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("200")); + assertFalse(headersFrame.isEndStream()); + + Http2DataFrame dataFrame = ch.readOutbound(); + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + assertTrue(dataFrame.isEndStream()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeEmptyFullResponseWithTrailers() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + HttpHeaders trailers = response.trailingHeaders(); + trailers.set("key", "value"); + assertTrue(ch.writeOutbound(response)); + + Http2HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("200")); + assertFalse(headersFrame.isEndStream()); + + Http2HeadersFrame trailersFrame = ch.readOutbound(); + assertThat(trailersFrame.headers().get("key").toString(), is("value")); + assertTrue(trailersFrame.isEndStream()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeNonEmptyFullResponseWithTrailers() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, hello); + HttpHeaders trailers = response.trailingHeaders(); + trailers.set("key", "value"); + assertTrue(ch.writeOutbound(response)); + + Http2HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("200")); + assertFalse(headersFrame.isEndStream()); + + Http2DataFrame dataFrame = ch.readOutbound(); + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + assertFalse(dataFrame.isEndStream()); + + Http2HeadersFrame trailersFrame = ch.readOutbound(); + assertThat(trailersFrame.headers().get("key").toString(), is("value")); + assertTrue(trailersFrame.isEndStream()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeHeaders() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + assertTrue(ch.writeOutbound(response)); + + Http2HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("200")); + assertFalse(headersFrame.isEndStream()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeChunk() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + HttpContent content = new DefaultHttpContent(hello); + assertTrue(ch.writeOutbound(content)); + + Http2DataFrame dataFrame = ch.readOutbound(); + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + assertFalse(dataFrame.isEndStream()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeEmptyEnd() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + LastHttpContent end = LastHttpContent.EMPTY_LAST_CONTENT; + assertTrue(ch.writeOutbound(end)); + + Http2DataFrame emptyFrame = ch.readOutbound(); + assertThat(emptyFrame.content().readableBytes(), is(0)); + assertTrue(emptyFrame.isEndStream()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeDataEnd() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + LastHttpContent end = new DefaultLastHttpContent(hello, true); + assertTrue(ch.writeOutbound(end)); + + Http2DataFrame dataFrame = ch.readOutbound(); + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + assertTrue(dataFrame.isEndStream()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeTrailers() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + LastHttpContent trailers = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, true); + HttpHeaders headers = trailers.trailingHeaders(); + headers.set("key", "value"); + assertTrue(ch.writeOutbound(trailers)); + + Http2HeadersFrame headerFrame = ch.readOutbound(); + assertThat(headerFrame.headers().get("key").toString(), is("value")); + assertTrue(headerFrame.isEndStream()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeDataEndWithTrailers() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + LastHttpContent trailers = new DefaultLastHttpContent(hello, true); + HttpHeaders headers = trailers.trailingHeaders(); + headers.set("key", "value"); + assertTrue(ch.writeOutbound(trailers)); + + Http2DataFrame dataFrame = ch.readOutbound(); + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + assertFalse(dataFrame.isEndStream()); + + Http2HeadersFrame headerFrame = ch.readOutbound(); + assertThat(headerFrame.headers().get("key").toString(), is("value")); + assertTrue(headerFrame.isEndStream()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDowngradeHeaders() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + Http2Headers headers = new DefaultHttp2Headers(); + headers.path("/"); + headers.method("GET"); + + assertTrue(ch.writeInbound(new DefaultHttp2HeadersFrame(headers))); + + HttpRequest request = ch.readInbound(); + assertThat(request.uri(), is("/")); + assertThat(request.method(), is(HttpMethod.GET)); + assertThat(request.protocolVersion(), is(HttpVersion.HTTP_1_1)); + assertFalse(request instanceof FullHttpRequest); + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDowngradeFullHeaders() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + Http2Headers headers = new DefaultHttp2Headers(); + headers.path("/"); + headers.method("GET"); + + assertTrue(ch.writeInbound(new DefaultHttp2HeadersFrame(headers, true))); + + FullHttpRequest request = ch.readInbound(); + assertThat(request.uri(), is("/")); + assertThat(request.method(), is(HttpMethod.GET)); + assertThat(request.protocolVersion(), is(HttpVersion.HTTP_1_1)); + assertThat(request.content().readableBytes(), is(0)); + assertTrue(request.trailingHeaders().isEmpty()); + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDowngradeTrailers() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + Http2Headers headers = new DefaultHttp2Headers(); + headers.set("key", "value"); + assertTrue(ch.writeInbound(new DefaultHttp2HeadersFrame(headers, true))); + + LastHttpContent trailers = ch.readInbound(); + assertThat(trailers.content().readableBytes(), is(0)); + assertThat(trailers.trailingHeaders().get("key").toString(), is("value")); + assertFalse(trailers instanceof FullHttpRequest); + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDowngradeData() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + assertTrue(ch.writeInbound(new DefaultHttp2DataFrame(hello))); + + HttpContent content = ch.readInbound(); + assertThat(content.content().toString(CharsetUtil.UTF_8), is("hello world")); + assertFalse(content instanceof LastHttpContent); + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDowngradeEndData() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + assertTrue(ch.writeInbound(new DefaultHttp2DataFrame(hello, true))); + + LastHttpContent content = ch.readInbound(); + assertThat(content.content().toString(CharsetUtil.UTF_8), is("hello world")); + assertTrue(content.trailingHeaders().isEmpty()); + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testPassThroughOther() throws Exception { + EmbeddedChannel ch = new EmbeddedChannel(new Http2ServerDowngrader()); + Http2ResetFrame reset = new DefaultHttp2ResetFrame(0); + Http2GoAwayFrame goaway = new DefaultHttp2GoAwayFrame(0); + assertTrue(ch.writeInbound(reset)); + assertTrue(ch.writeInbound(goaway)); + + assertEquals(ch.readInbound(), reset); + assertEquals(ch.readInbound(), goaway); + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } +}