Cancel DelayedClientCall when application listener throws#12761
Merged
kannanjgithub merged 3 commits intogrpc:masterfrom Apr 22, 2026
Merged
Conversation
Align DelayedClientCall.DelayedListener with ClientCallImpl's existing behavior for listener exceptions. When the application listener throws from onHeaders/onMessage/onReady, catch the Throwable, cancel the call with CANCELLED (cause = the throwable), and swallow subsequent callbacks. When onClose throws, log and continue, matching ClientCallImpl.closeObserver. If onClose arrives from the transport after a prior callback threw, override its status/trailers with the captured CANCELLED so a server-supplied OK can't mask the local failure. Previously, a throw from the application listener escaped to the callExecutor's uncaught-exception handler. The real call was not cancelled and the transport kept delivering callbacks to an already broken listener, different from how the same bug behaves on a normal ClientCallImpl, and a timing-dependent inconsistency depending on whether callbacks arrived before or after setCall + drain completed. Trade-off: listener-callback throws are no longer visible to the executor's UncaughtExceptionHandler (they're attached as Status.cause instead). This matches ClientCallImpl and is the intended behavior. Exception handling for the outer drainPendingCalls loop (realCall.sendMessage/request/halfClose/cancel) remains unaddressed; that TODO is preserved.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR aligns DelayedClientCall.DelayedListener behavior with ClientCallImpl when the application ClientCall.Listener throws from callbacks, ensuring the call is cancelled and subsequent callbacks are suppressed/normalized instead of escaping to the executor’s uncaught exception handler.
Changes:
- Add exception-handling wrappers for
onHeaders,onMessage, andonReadythat cancel the call withCANCELLED(cause = thrown exception) and swallow later callbacks. - Override transport-delivered
onClose(Status.OK, …)after a prior listener exception to prevent server OK from masking a local failure; keeponCloseexception behavior as “log and continue.” - Add unit tests covering pending vs pass-through callback paths and the
onCloseoverride behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| core/src/main/java/io/grpc/internal/DelayedClientCall.java | Wraps listener callback dispatch to catch listener-thrown exceptions, cancel the real call, and normalize subsequent callbacks/onClose to match ClientCallImpl semantics. |
| core/src/test/java/io/grpc/internal/DelayedClientCallTest.java | Adds targeted tests for listener-throwing scenarios (pending + pass-through) and verifies cancellation + callback swallowing/onClose override. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+543
to
+547
| // Ideally exceptionStatus == status, as exceptionStatus was passed to cancel(). | ||
| // However the cancel is racy and this onClose may have already been queued when the | ||
| // cancellation occurred. Since other callbacks throw away data if exceptionStatus != | ||
| // null, it is semantically essential that we _not_ use a status provided by the | ||
| // server. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
kannanjgithub
approved these changes
Apr 21, 2026
Contributor
|
/gcbrun |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Align DelayedClientCall.DelayedListener with ClientCallImpl's existing behavior for listener exceptions. When the application listener throws from onHeaders/onMessage/onReady, catch the Throwable, cancel the call with CANCELLED (cause = the throwable), and swallow subsequent callbacks. When onClose throws, log and continue, matching ClientCallImpl.closeObserver. If onClose arrives from the transport after a prior callback threw, override its status/trailers with the captured CANCELLED so a server-supplied OK can't mask the local failure.
Previously, a throw from the application listener escaped to the callExecutor's uncaught-exception handler. The real call was not cancelled and the transport kept delivering callbacks to an already broken listener, different from how the same bug behaves on a normal ClientCallImpl, and a timing-dependent inconsistency depending on whether callbacks arrived before or after setCall + drain completed.
Trade-off: listener-callback throws are no longer visible to the executor's UncaughtExceptionHandler (they're attached as Status.cause instead). This matches ClientCallImpl and is the intended behavior.
Exception handling for the outer drainPendingCalls loop (realCall.sendMessage/request/halfClose/cancel) remains unaddressed; that TODO is preserved.
Note:
This change only handles exceptions thrown by the application listener. I don't try and solve the problems that #12737 is attempting to fix. My motivation is to fix the root cause behind bazelbuild/bazel#29316