fix: route JDK transport async dispatch failures through the returned future#134
fix: route JDK transport async dispatch failures through the returned future#134OmarAlJarrah wants to merge 1 commit into
Conversation
JdkHttpTransport.executeAsync guarded only the request-adaptation call. The dispatch kickoff that follows on the caller's thread — HttpClient.sendAsync plus the bridgeAsyncResponse wiring — ran unguarded. sendAsync does not guarantee that every failure reaches its returned future: on a closed java.net.http.HttpClient (JDK 21+, where the client is AutoCloseable) it throws synchronously, so the exception escaped on the caller's thread and bypassed the future. That breaks the method's documented contract that all errors arrive through the returned CompletableFuture, and a future-composing caller's .exceptionally/.handle would never observe such a throw. Widen the try/catch to enclose the whole post-adaptation dispatch path so any synchronous throw becomes a failed future. The OkHttp transport is already correct here — newCall does no throwing work and dispatch failures arrive via Callback.onFailure — so it is left unchanged apart from a comment recording why its post-adapt path is future-safe. The catch stays at Exception (not RuntimeException) in both transports: the intent is that any non-fatal failure, including an unexpected adapter bug such as an NPE, surfaces via the future; only Error / JVM-fatal conditions propagate. The inline comments now state that breadth is deliberate. Both transports' async adaptation-failure tests now assert future.isCompletedExceptionally() is already true on return (completion is synchronous, not merely eventual) and assert a substring of the adapter's message so an unrelated IllegalArgumentException cannot satisfy the test. A new JDK test closes an AutoCloseable client and asserts the synchronous sendAsync throw is delivered through the future. Closes #120 Closes #121 Closes #122
|
This widens IssuesNew dispatch-failure test doesn't actually exercise the post-adapt guard — Production comment overstates the risk it's fixing — |
Problem
JdkHttpTransport.executeAsyncis documented to deliver every error through the returnedCompletableFuture, never by throwing on the caller's thread. Today the try/catch wraps only therequestAdapter.adapt(...)call. The dispatch kickoff that runs immediately after —client.sendAsync(...)plus thebridgeAsyncResponse(...)wiring — runs unguarded on the caller's thread before the future is handed back.sendAsyncdoes not guarantee that every failure arrives through its returned future. On a closedjava.net.http.HttpClient(JDK 21+, where the client isAutoCloseable) it throws synchronously, so the exception escapes on the caller's thread and bypasses the future entirely. A future-composing caller's.exceptionally/.handlewould never observe it, breaking the method's contract.The OkHttp transport is already correct here:
newCall(...)does no throwing work, and a dispatch failure (including aRejectedExecutionExceptionfrom a shut-down dispatcher) is delivered throughCallback.onFailure, so it already reaches the future.Change
executeAsynctry/catch to enclose the whole post-adaptation dispatch path (sendAsync+bridgeAsyncResponse), so any synchronous throw becomes a failed future rather than escaping on the caller's thread.Exception(notRuntimeException). The intent is that any non-fatal failure, including an unexpected adapter bug such as an NPE, surfaces via the future; onlyError/ JVM-fatal conditions propagate. The inline comments now state that breadth is deliberate.future.isCompletedExceptionally()is already true on return (completion is synchronous, not merely eventual) and assert a substring of the adapter's message so an unrelatedIllegalArgumentExceptioncannot satisfy the test. A new JDK test closes anAutoCloseableclient and asserts the synchronoussendAsyncthrow is delivered through the future (skipped on JDK < 21, where the client has no close hook).No public-API change.
Gated build commands run
Result: BUILD SUCCESSFUL. (detekt is skipped on
sdk-transport-jdkhttpper the module's toolchain exclusion.)Closes #120
Closes #121
Closes #122