Skip to content

Reject unary calls when the http/2 stream closes with NO_ERROR before the response - #1747

Open
jackyzha0 wants to merge 1 commit into
connectrpc:mainfrom
jackyzha0:fix-orphaned-unary-pre-response
Open

Reject unary calls when the http/2 stream closes with NO_ERROR before the response#1747
jackyzha0 wants to merge 1 commit into
connectrpc:mainfrom
jackyzha0:fix-orphaned-unary-pre-response

Conversation

@jackyzha0

Copy link
Copy Markdown

Background

Fixes #1678.

The close handler in h2Request() only errors the sentinel when the stream's reset code maps to a ConnectError, and connectErrorFromH2ResetCode() intentionally maps NO_ERROR (0x0) to undefined, because that is also the code of ordinary stream completion.

But when a stream is reset with NO_ERROR before response headers were received - proxies like Linkerd or Envoy do this during graceful shutdowns - there is no ordinary completion: the response promise has not resolved, and nothing else errors the sentinel. The returned promise stays pending forever, which matches the forensics in #1678 (one orphaned await, session manager already moved on, no retained streams).

Summary

Track whether the response event was seen, and reject with Code.Unavailable when the stream closes without it and without a mapped reset error. Streams that received a response are unaffected, so ordinary completions (which also close with code 0x0) don't change behavior.

Unavailable matches the existing REFUSED_STREAM mapping: the server provably did not process the request, so it is safe to retry.

The new test reproduces the hang with a server that resets the stream with NGHTTP2_NO_ERROR without responding; without the fix, the response promise never settles.

Related, but distinct from #1746: that one fixes a session-level hang in Http2SessionManager.verify(); this one fixes a call-level hang in the universal client.

… the response

The "close" handler in h2Request() only errors the sentinel when the
stream's reset code maps to a ConnectError, and
connectErrorFromH2ResetCode() intentionally maps NO_ERROR (0x0) to
undefined, since that is also the code of ordinary stream completion.
But when a stream is reset with NO_ERROR before response headers were
received - proxies do this during graceful shutdowns - there is no
ordinary completion: the response promise has not resolved, and nothing
else errors the sentinel. The returned promise stays pending forever.

Track whether the "response" event was seen, and reject with
Code.Unavailable when the stream closes without it and without a mapped
reset error. Unavailable matches the REFUSED_STREAM mapping: the server
provably did not process the request, so it is safe to retry. Streams
that received a response are unaffected.

Fixes connectrpc#1678.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Jacky Zhao <j.zhao2k19@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unary Promise never settles — sentinel.error never called after premature stream termination

1 participant