Skip to content

Stream headers frame sent before HTTP/2 connection preface/settings #16635

@ravi-signal

Description

@ravi-signal

I have an application where I'm using Http2MultiplexHandler/Http2StreamChannelBootstrap create an H2 connection to an H2 server from within an event loop.

The example blocks when constructing the initial H2 connection and then calls Http2StreamChannelBootstrap#open. I've found that also works fine for me.

However when I instead try opening the stream from a listener on the initial connect, when I call open and then send a headers frame on the new stream, my server always gets the stream headers before the HTTP/2 preface

WARNING: An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.                                               
io.netty.handler.codec.http2.Http2Exception: HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 0000090105000000038244052f7465737486
        at io.netty.handler.codec.http2.Http2Exception.connectionError(Http2Exception.java:98)
        at io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.readClientPrefaceString(Http2ConnectionHandler.java:316)
        at io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.decode(Http2ConnectionHandler.java:245)
        at io.netty.handler.codec.http2.Http2ConnectionHandler.decode(Http2ConnectionHandler.java:462)
        at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:545)
        at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:484)
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429)
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:176)
        at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445)
        at io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:388)
        at io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:596)
        at io.netty.channel.nio.NioIoHandler.processSelectedKeysOptimized(NioIoHandler.java:571)
        at io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:512)
        at io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:484)
        at io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:225)
        at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:196)
        at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1195)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:1447)         

I've also found if I just schedule that open call back on the event loop, that also ensures the preface is sent before the header. That is:

// works
c = boostrap.connect().sync().channel()
new Http2StreamChannelBootstrap(c).open().addListener(.. send request on stream);

// does not work
boostrap.connect().addListener(f -> new Http2StreamChannelBootstrap(f.channel()).open().addListener(.. send request on stream...));

// also works
boostrap.connect().addListener(eventLoop.submit(() -> f -> new Http2StreamChannelBootstrap(f.channel()).open().addListener(.. send request on stream...)));

Naively, I would expect open to ensure the preface has been enqueued before returning. I don't necessarily think this is a bug (maybe a documentation issue), but I'm not sure how to to make sure that the connection preface+settings has been buffered before I send a stream header. I could wait to recieve the server setting's frame, but I'd like to send as soon as possible (so just after we send the prefix/setting).

I've put a reproduction of the behavior in https://gist.github.com/ravi-signal/267df7b2d5fd9b18386fff47d9ae0a8f with 3 unit tests:

openStreamAfterBlockingConnect blocks and always sends the preface before the headers frame
openStreamAfterConnectListen opens in the connect listener and always sends the headers frame before the preface
openStreamAfterConnectListenOnEventLoop opens in the connect listener after dispatching back to the event loop, and always sends the preface before the headers frame.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions