NullPointerException in NettyClientHandler.onHeadersRead when a HEADERS frame races concurrent stream teardown
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 76/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- grpc, java
調査の方向性
Start in NettyClientHandler.java with onHeadersRead, then compare onDataRead and onRstStreamRead with the server-side guard pattern from #10384. Verify behavior when clientStream() returns null after stream teardown; done means cleared TransportState is handled without a null dereference across the affected callbacks.
索引モデルが issue の本文から書いたものです。
説明
What version of gRPC-Java are you using?
1.69.0 (confirmed the same unguarded code is still present at HEAD of the latest release, 1.84.0)
What did you see instead?
A NullPointerException inside NettyClientHandler.onHeadersRead, wrapped by google-cloud-pubsub's StreamingSubscriberConnection:
com.google.api.gax.rpc.UnknownException: io.grpc.StatusRuntimeException: UNKNOWN
at com.google.api.gax.rpc.ApiExceptionFactory.createException(ApiExceptionFactory.java:119)
...
Caused by: io.grpc.StatusRuntimeException: UNKNOWN
at io.grpc.Status.asRuntimeException(Status.java:532)
... 16 common frames omitted
Caused by: java.lang.NullPointerException: Cannot invoke "io.grpc.netty.NettyClientStream$TransportState.tag()" because "stream" is null
at io.grpc.netty.NettyClientHandler.onHeadersRead(NettyClientHandler.java:400)
at io.grpc.netty.NettyClientHandler$FrameListener.onHeadersRead(NettyClientHandler.java:981)
at io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder$FrameReadListener.onHeadersRead(DefaultHttp2ConnectionDecoder.java:409)
...
Observed on a com.google.cloud:google-cloud-pubsub:1.134.2 Subscriber (StreamingPull), during a burst where ~16 subscriber connections were being established concurrently within a ~10s window (a batch of pods restarting and each re-subscribing to many Pub/Sub subscriptions at once).
What did you expect to see?
No NPE — a stream whose TransportState has already been cleared should be handled gracefully, the same way it's handled elsewhere in this same class.
Root cause (traced from source)
NettyClientHandler.java:399—NettyClientStream.TransportState stream = clientStream(requireHttp2Stream(streamId));NettyClientHandler.java:400—PerfMark.event("NettyClientHandler.onHeadersRead", stream.tag());— dereferencesstreamwith no null check.NettyClientHandler.java:930-931—clientStream():
This returnsprivate NettyClientStream.TransportState clientStream(Http2Stream stream) { return stream == null ? null : (NettyClientStream.TransportState) stream.getProperty(streamKey); }nullwhenever theHttp2Streamobject is still live in the connection (sorequireHttp2Streamdoesn't throw) but itsTransportStateproperty was already cleared — e.g. a stream-cancel/RST race. That's exactly the condition hit when a HEADERS frame for that stream arrives concurrently with its own teardown.
The same unguarded clientStream(requireHttp2Stream(streamId)) → immediate dereference pattern also exists at:
NettyClientHandler.java:436-437(onDataRead)NettyClientHandler.java:448-450(onRstStreamRead)
Suggested fix
This looks like the same class of bug as #10364, which was fixed server-side in #10384 by adding a null guard right after the serverStream(requireHttp2Stream(streamId)) call:
NettyServerStream.TransportState stream = serverStream(requireHttp2Stream(streamId));
if (stream == null) {
return;
}
The same guard pattern would apply to NettyClientHandler's onHeadersRead (and likely onDataRead/onRstStreamRead, which have the identical shape).
Impact observed
Low — this was WARN-level in our application logs (not fatal), and the underlying gax retry logic (UNKNOWN status is retryable) recovered the stream automatically. Filing for visibility since the null-dereference itself is a real, reachable bug with a known-good fix pattern already established for the server-side sibling.
- 主要言語
- Java
- スター
- 12.1k
- フォーク
- 4k
- 平均マージ
- 2日 3時間
- マージ済み PR(30日)
- 32
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
grpc/grpc-java のほかの issue
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
grpc/grpc-java#13063 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
grpc/grpc-java#13052 · コメント 4 件 ·
メンテナーはふだん 1 日以内に返信
-
docs enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
grpc/grpc-java#10824 · コメント 8 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 65/100
grpc/grpc-java#13075 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
question
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
grpc/grpc-java#13066 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
beehive-lab/TornadoVM#1151 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 77/100
FasterXML/jackson-dataformats-binary#823 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信