Skip to content

CXF-9257: Complete client futures if the async callback throws - #3565

Merged
reta merged 2 commits into
apache:mainfrom
markusheiden:complete-on-throw
Oct 11, 2026
Merged

reta merged 2 commits into
apache:mainfrom
markusheiden:complete-on-throw

Conversation

@markusheiden

@markusheiden markusheiden commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

If the callback of an asynchronous client invocation throws, the future returned by the invocation may never complete, so threads blocked in Future.get() hang forever.

CXF callers are expected to invoke ClientCallback.handleException() if handleResponse() throws. This PR:

  • fixes the call sites that don't follow that contract: WebClient's ClientAsyncResponseInterceptor and the fault observer wrapper in ClientImpl;
  • guards the failure paths inside JaxrsClientCallback and JaxwsClientCallback (handleException(), cancel(), the interruption path of JaxrsResponseFuture.get()). Callers can't complete the future if handleException() itself throws, because the future is owned by the callback. If the user's callback throws there, the original exception is kept and the callback's exception is attached as suppressed (unless it is the same instance).

Unit tests for both callbacks and a WebClient system test in JAXRSAsyncClientTest are included. Details in https://issues.apache.org/jira/browse/CXF-9257.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ka6VjhpAcDrSERoiLP2Cxx

JaxrsClientCallback and JaxwsClientCallback invoke the user's callback
before completing their future. If the callback throws, the exception
escapes and the future is never completed, so threads blocked in
Future.get() hang forever. In the JAX-WS case ClientImpl additionally
catches the exception and invokes the same callback a second time.

Guard every callback invocation:
- If the callback throws while handling a successful response, the
  future completes exceptionally with that exception, like the JAX-WS
  reference implementation does.
- If the callback throws while handling a failure, cancellation or
  interruption, the original exception is kept and the callback's
  exception is attached as a suppressed exception.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149ipFQyLFL9z5WdmhCRg3r
@markusheiden markusheiden changed the title Complete client futures if the async callback throws CXF-9257: Complete client futures if the async callback throws Oct 10, 2026
@reta

reta commented Oct 10, 2026

Copy link
Copy Markdown
Member

Thanks for the change @markusheiden , the way CXF manages callback:

                try {
                    callback.handleResponse(resCtx, obj);
                } catch (Throwable ex) {
                    callback.handleException(resCtx, ex);
                }

There are some gaps in WebClient indeed which violate that (like you mentioned, ClientAsyncResponseInterceptor), I think the way to fix the issue would be to change ClientAsyncResponseInterceptor (and possibly other places) but not the callbacks. Hope it make sense.

CXF callers invoke ClientCallback.handleException() if handleResponse()
throws, so the callbacks don't need to guard the success path. Revert
those guards and fix the call sites that don't follow the contract:
ClientImpl's fault observer wrapper and WebClient's
ClientAsyncResponseInterceptor.

The failure paths stay guarded in the callbacks, because callers can't
complete the future if handleException() throws. Don't add a callback
exception as suppressed to itself, which would throw
IllegalArgumentException if the callback rethrows the exception it was
given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ka6VjhpAcDrSERoiLP2Cxx
@markusheiden

markusheiden commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the fast review @reta. I've reverted the guards in handleResponse and instead fixed the call sites that don't follow the contract (WebClient's ClientAsyncResponseInterceptor and the fault observer wrapper in ClientImpl), with a system test in JAXRSAsyncClientTest. I kept the guards on the failure paths in the callbacks (handleException, cancel, and the interrupted get): if the user's handler throws there, the caller has no way left to complete the future, since it belongs to the callback. In the original BingAds case, the AsyncHandler throws again when it's invoked from handleException, so the future still hung.

I added a table comparing the old and the new approach to https://issues.apache.org/jira/browse/CXF-9257 .

@reta reta left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @markusheiden

@reta
reta merged commit cca9715 into apache:main Oct 11, 2026
5 checks passed
reta pushed a commit that referenced this pull request Oct 11, 2026
* Complete client futures if the async callback throws

JaxrsClientCallback and JaxwsClientCallback invoke the user's callback
before completing their future. If the callback throws, the exception
escapes and the future is never completed, so threads blocked in
Future.get() hang forever. In the JAX-WS case ClientImpl additionally
catches the exception and invokes the same callback a second time.

Guard every callback invocation:
- If the callback throws while handling a successful response, the
  future completes exceptionally with that exception, like the JAX-WS
  reference implementation does.
- If the callback throws while handling a failure, cancellation or
  interruption, the original exception is kept and the callback's
  exception is attached as a suppressed exception.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149ipFQyLFL9z5WdmhCRg3r

* Follow the callback contract instead of guarding handleResponse

CXF callers invoke ClientCallback.handleException() if handleResponse()
throws, so the callbacks don't need to guard the success path. Revert
those guards and fix the call sites that don't follow the contract:
ClientImpl's fault observer wrapper and WebClient's
ClientAsyncResponseInterceptor.

The failure paths stay guarded in the callbacks, because callers can't
complete the future if handleException() throws. Don't add a callback
exception as suppressed to itself, which would throw
IllegalArgumentException if the callback rethrows the exception it was
given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ka6VjhpAcDrSERoiLP2Cxx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit cca9715)
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.

2 participants