netty: HTTP/2 Prior Knowledge does not appear to work with Http2Codec and Http2ServerDowngrader

Expected behavior

The CleartextHttp2ServerUpgradeHandler should work with both HTTP Upgrade and Prior Knowledge when downgrading HTTP/2 requests internally.

Actual behavior

HTTP Upgrade works, but not Prior Knowledge. In the latter case, nghttp reports: (last_stream_id=0, error_code=PROTOCOL_ERROR(0x01), opaque_data(96)=[HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 000000040100000000])

Steps to reproduce

  1. Fire up io.netty.example.http2.helloworld.server.Http2Server with the modifications described in the next section
  2. Install nghttp
  3. Run nghttp --upgrade --verbose --no-dep http://localhost:8080/; it should succeed
  4. Run nghttp --verbose --no-dep http://localhost:8080/; it fails like so:
[  0.002] Connected
[  0.002] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.002] send HEADERS frame <length=33, flags=0x05, stream_id=1>
          ; END_STREAM | END_HEADERS
          (padlen=0)
          ; Open new stream
          :method: GET
          :path: /
          :scheme: http
          :authority: localhost:8080
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.23.1
[  0.005] recv SETTINGS frame <length=6, flags=0x00, stream_id=0>
          (niv=1)
          [SETTINGS_MAX_HEADER_LIST_SIZE(0x06):8192]
[  0.005] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.006] recv GOAWAY frame <length=104, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=PROTOCOL_ERROR(0x01), opaque_data(96)=[HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 000000040100000000])
Some requests were not processed. total=1, processed=0

Minimal yet complete reproducer code (or URL to code)

Run the Http2Server example code, using the following modified configureClearText method:

    private void configureClearText(SocketChannel ch) {
        final ChannelPipeline p = ch.pipeline();
        final HttpServerCodec sourceCodec = new HttpServerCodec();

        final Http2Codec http2Codec = new Http2CodecBuilder(true, new ChannelInitializer<Channel>() {
            @Override
            protected void initChannel(Channel channel) throws Exception {
                ChannelPipeline p = channel.pipeline();
                p.addLast("downgrader", new Http2ServerDowngrader(true));
                p.addLast("chunk-aggregator", new HttpObjectAggregator(1048576));
                p.addLast("http1-handler", new HelloWorldHttp1Handler("Downgrade"));
            }
        }).build();

        final UpgradeCodecFactory upgradeCodecFactory = new UpgradeCodecFactory() {
            @Override
            public UpgradeCodec newUpgradeCodec(CharSequence protocol) {
                if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) {
                    return new Http2ServerUpgradeCodec("server-upgrade-codec", http2Codec);
                } else {
                    return null;
                }
            }
        };

        final HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory);

        final CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler =
            new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, http2Codec);

        p.addLast(cleartextHttp2ServerUpgradeHandler);

        p.addLast(new SimpleChannelInboundHandler<HttpMessage>() {
            @Override
            protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception {
                // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP.
                System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)");
                ChannelPipeline pipeline = ctx.pipeline();
                ChannelHandlerContext thisCtx = pipeline.context(this);
                pipeline.addAfter(thisCtx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
                pipeline.replace(this, null, new HttpObjectAggregator(maxHttpContentLength));
                ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
            }
        });

        p.addLast(new UserEventLogger());
    }

Netty version

Latest 4.1 branch

JVM version (e.g. java -version)

java 8u131

OS version (e.g. uname -a)

macOS 10.12.4

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 16 (12 by maintainers)

Commits related to this issue

Most upvoted comments

It sounds like there’s a much deeper design issue here. Why does Netty allow a Handler to pass an event via its ChannelHandlerContext when that Handler is no longer present in the pipeline and therefore does not have a defined position in that pipeline? After all, the ChannelHandlerContext represents a Handler’s association with a pipeline–shouldn’t it throw an IllegalStateException as soon as you try to do this?

And as for the various HTTP2 pipeline components, do they really need to rely on pipeline mutation so heavily? Your root cause analysis seems like a great example of how complex mutation-oriented programming can be even in a single-threaded context.

As for a fix, can PriorKnowledgeHandler add Http2Codec after itself (instead of calling replace), propagate the data down the pipeline, and then remove itself from the pipeline?