netty / netty-incubator-codec-http3

Experimental HTTP3 codec on top of QUIC
Apache License 2.0
170 stars 35 forks source link

QpackEncoder.streamCancellation() throws a NullPointerException #252

Closed nlifew closed 11 months ago

nlifew commented 1 year ago

I have a nginx http3 server listening localhost 8443 port. When I run http3 client example code, it throws a NullPoiterException. I tried quiche's example http client, that works fine. The following information may be helpful.

Environment: os: macos ventura 13.3 cpu: arm64 m2 java: "11.0.18" 2023-01-17 LTS

My Example Code I copied from here and converted it to kotlin.

val group = NioEventLoopGroup(1)

    try {
        val context = QuicSslContextBuilder.forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE)
            .applicationProtocols(*Http3.supportedApplicationProtocols())
            .build()
        val codec = Http3.newQuicClientCodecBuilder()
            .sslContext(context)
            .maxIdleTimeout(5000, TimeUnit.MILLISECONDS)
            .initialMaxData(10000000)
            .initialMaxStreamDataBidirectionalLocal(1000000)
            .build()

        val bs = Bootstrap()
        val channel = bs.group(group)
            .channel(NioDatagramChannel::class.java)
                .handler(codec)
                .bind(0).sync().channel()

        val quicChannel = QuicChannel.newBootstrap(channel)
            .handler(Http3ClientConnectionHandler())
            .remoteAddress(InetSocketAddress(NetUtil.LOCALHOST4, 8443))
            .connect()
            .get()

        val streamChannel = Http3.newRequestStream(quicChannel, object: Http3RequestStreamInboundHandler() {

            override fun channelRead(ctx: ChannelHandlerContext, frame: Http3HeadersFrame) {
                ReferenceCountUtil.release(frame)
            }

            override fun channelRead(ctx: ChannelHandlerContext, frame: Http3DataFrame) {
                System.err.print(frame.content().toString(CharsetUtil.US_ASCII));
                ReferenceCountUtil.release(frame);
            }

            override fun channelInputClosed(ctx: ChannelHandlerContext) {
                ctx.close()
            }
        }).sync().getNow()

        // Write the Header frame and send the FIN to mark the end of the request.
        // After this its not possible anymore to write any more data.
        val frame = DefaultHttp3HeadersFrame()
        frame.headers().method("GET").path("/")
            .authority(/*NetUtil.LOCALHOST4.getHostAddress() + */"localhost:" + 8443)
            .scheme("https")
        streamChannel.writeAndFlush(frame)
            .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT).sync()

        // Wait for the stream channel and quic channel to be closed (this will happen after we received the FIN).
        // After this is done we will close the underlying datagram channel.
        streamChannel.closeFuture().sync()

        // After we received the response lets also close the underlying QUIC channel and datagram channel.
        quicChannel.close().sync()
        channel.close().sync()
    } finally {
        group.shutdownGracefully()
    }

StackTrace

io.netty.channel.DefaultChannelPipeline onUnhandledInboundException
Warn: 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.DecoderException: java.lang.NullPointerException
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:499)
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
    at io.netty.handler.codec.ByteToMessageDecoder.handlerRemoved(ByteToMessageDecoder.java:266)
    at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:536)
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:468)
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
    at io.netty.incubator.codec.quic.QuicheQuicStreamChannel$QuicStreamChannelUnsafe.recv(QuicheQuicStreamChannel.java:908)
    at io.netty.incubator.codec.quic.QuicheQuicStreamChannel$QuicStreamChannelUnsafe.beginRead(QuicheQuicStreamChannel.java:603)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.read(DefaultChannelPipeline.java:1362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeRead(AbstractChannelHandlerContext.java:835)
    at io.netty.channel.AbstractChannelHandlerContext.read(AbstractChannelHandlerContext.java:814)
    at io.netty.channel.DefaultChannelPipeline.read(DefaultChannelPipeline.java:1004)
    at io.netty.channel.DefaultChannelPipeline.read(DefaultChannelPipeline.java:46)
    at io.netty.incubator.codec.quic.QuicheQuicStreamChannel.read(QuicheQuicStreamChannel.java:287)
    at io.netty.incubator.codec.quic.QuicheQuicStreamChannel.read(QuicheQuicStreamChannel.java:51)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.readIfIsAutoRead(DefaultChannelPipeline.java:1422)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelActive(DefaultChannelPipeline.java:1400)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:258)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:238)
    at io.netty.channel.DefaultChannelPipeline.fireChannelActive(DefaultChannelPipeline.java:895)
    at io.netty.incubator.codec.quic.QuicheQuicStreamChannel$QuicStreamChannelUnsafe.register(QuicheQuicStreamChannel.java:475)
    at io.netty.channel.SingleThreadEventLoop.register(SingleThreadEventLoop.java:89)
    at io.netty.channel.SingleThreadEventLoop.register(SingleThreadEventLoop.java:83)
    at io.netty.incubator.codec.quic.QuicheQuicChannel$2.onUnhandledInboundMessage(QuicheQuicChannel.java:452)
    at io.netty.channel.DefaultChannelPipeline$TailContext.channelRead(DefaultChannelPipeline.java:1296)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
    at io.netty.incubator.codec.http3.Http3ConnectionHandler.channelRead(Http3ConnectionHandler.java:172)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
    at io.netty.incubator.codec.quic.QuicheQuicChannel$QuicChannelUnsafe.recvStream(QuicheQuicChannel.java:1569)
    at io.netty.incubator.codec.quic.QuicheQuicChannel$QuicChannelUnsafe.processReceived(QuicheQuicChannel.java:1502)
    at io.netty.incubator.codec.quic.QuicheQuicChannel$QuicChannelUnsafe.connectionRecv(QuicheQuicChannel.java:1444)
    at io.netty.incubator.codec.quic.QuicheQuicChannel.recv(QuicheQuicChannel.java:864)
    at io.netty.incubator.codec.quic.QuicheQuicCodec$QuicCodecHeaderProcessor.process(QuicheQuicCodec.java:274)
    at io.netty.incubator.codec.quic.QuicHeaderParser.parse(QuicHeaderParser.java:127)
    at io.netty.incubator.codec.quic.QuicheQuicCodec.handleQuicPacket(QuicheQuicCodec.java:143)
    at io.netty.incubator.codec.quic.QuicheQuicCodec.channelRead(QuicheQuicCodec.java:134)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
    at io.netty.channel.nio.AbstractNioMessageChannel$NioMessageUnsafe.read(AbstractNioMessageChannel.java:97)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
    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:834)
Caused by: java.lang.NullPointerException
    at io.netty.incubator.codec.http3.QpackEncoder.streamCancellation(QpackEncoder.java:156)
    at io.netty.incubator.codec.http3.QpackDecoderHandler.decode(QpackDecoderHandler.java:84)
    at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529)
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:468)
    ... 69 more

Gradle Dependency

dependencies {
//    api project(':netty-quic')
    api('io.netty:netty-all:4.1.90.Final')
//    api 'io.netty.incubator:netty-incubator-codec-native-quic:0.0.51.Final:osx-aarch_64'
    api 'io.netty.incubator:netty-incubator-codec-http3:0.0.21.Final'
    api 'com.google.code.gson:gson:2.10.1'
    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}

nginx server config

server {
    listen       8443 ssl http2;
    listen       8443 quic reuseport;
    server_name  localhost;

#   http2 on;

    ssl_certificate     /etc/ca/server/server-cert.pem;
    ssl_certificate_key /etc/ca/server/server-key.pem;

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    ssl_protocols   TLSv1.3;
    add_header Alt-Svc 'h3=":8443"; ma=2592000,h3-29=":8443"; ma=2592000,h3-Q050=":8443"; ma=2592000,h3-Q046=":8443"; ma=2592000,h3-Q043=":8443"; ma=2592000,quic=":8443"; ma=2592000; v="46,43"';

    location / {
        root   html;
        index  index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

quiche's http3 example client screenshot

image

Thank you very much

normanmaurer commented 1 year ago

interesting.. seems like it tried to cancel a stream before configure the dynamic table.

normanmaurer commented 1 year ago

@nlifew also would it be possible to share a docker image or so with the nginx server running ?

normanmaurer commented 1 year ago

I guess maybe I could use https://hub.docker.com/r/patrikjuvonen/docker-nginx-http3 ?

nlifew commented 1 year ago

@nlifew also would it be possible to share a docker image or so with the nginx server running ?

Sorry, I'm too late. I cost about 1 day to learn how to build a docker image, but something was wrong, I got a 1.36GB size docker image finally…

I never tried docker before, so my nginx server was running on a physical machine. I compiled it by a shell script:

git clone --depth 1 https://github.com/google/boringssl.git
mkdir boringssl/build && cd boringssl/build
make -j8

wget -O nginx.tar.gz https://hg.nginx.org/nginx-quic/archive/tip.tar.gz
tar -zxvf nginx.tar.gz && cd `ls nginx-`
./auto/configure --prefix=/root/nginx --with-http_ssl_module --with-http_v2_module --with-http_v3_module --with-cc-opt="-I../boringssl-master/include" --with-ld-opt="-L../boringssl-master/build/ssl -L../boringssl-master/build/crypto"
make -j8 
make install
nlifew commented 1 year ago

I guess maybe I could use https://hub.docker.com/r/patrikjuvonen/docker-nginx-http3 ?

I will try this docker image, and see if the problem persists. Thank you very much.

nlifew commented 1 year ago

@normanmaurer I tried , it works, but nothing on the screen. It created a connection, then quit silently. Quiche's example http3 client works well, I really don't know why.

image

If you need any information next, I'm happy to provide.

normanmaurer commented 1 year ago

@nlifew so you can only reproduce it when you use your own compiled version... correct ?

nlifew commented 1 year ago

@nlifew so you can only reproduce it when you use your own compiled version... correct ?

@normanmaurer Yes. I will close this issue. Thank you very much.