Repository navigation
[BUG] azure-core-http-vertx: VertxRequestWriteSubscriber.endRequest calls promise::fail and can throw "Result is already complete" #50964
Description
Describe the bug
VertxRequestWriteSubscriber.endRequest fails the shared request promise with Promise.fail:
// sdk/core/azure-core-http-vertx/src/main/java/com/azure/core/http/vertx/implementation/VertxRequestWriteSubscriber.java
private void endRequest(int finishingWriteSize) {
...
end.get().onFailure(promise::fail);
}The promise has other completers: VertxHttpClient fails it from the response future (responseFuture.onFailure(promise::tryFail)), and so do the write timeout and cancellation. If one of them completes the promise first, Promise.fail throws IllegalStateException: Result is already complete inside the onFailure listener. On Vert.x this reaches the context exception handler as an uncaught exception.
This bug class has been fixed twice without covering this call site: #43399 / #43402 hardened resetRequest in the same class with tryFail, and #50821 switched all completions in VertxHttpClient to tryFail/tryComplete. endRequest was left out both times. It is unchanged on main (9970d5b, 1.3.0-beta.1).
Exception or Stack Trace
We hit this exception in production on 1.2.0 through the response path in VertxHttpClient, which #50821 fixes. It shows the same mechanism: a connection close fails the promise twice.
ERROR [com.azure.core.http.vertx.VertxHttpClient] (executor-thread-31770) java.io.IOException: io.vertx.core.http.HttpClosedException: Connection was closed
ERROR [io.quarkus.vertx.core.runtime.VertxCoreRecorder] (vert.x-eventloop-thread-1) Uncaught exception received by Vert.x: java.lang.IllegalStateException: Result is already complete
at io.vertx.core.Promise.fail(Promise.java:90)
at io.vertx.core.impl.future.FutureImpl$2.onFailure(FutureImpl.java:117)
at io.vertx.core.impl.future.FutureImpl$ListenerArray.onFailure(FutureImpl.java:316)
at io.vertx.core.impl.future.FutureBase.lambda$emitFailure$1(FutureBase.java:75)
at io.vertx.core.impl.ContextImpl.execute(ContextImpl.java:312)
at io.vertx.core.impl.DuplicatedContext.execute(DuplicatedContext.java:168)
at io.vertx.core.impl.future.FutureBase.emitFailure(FutureBase.java:72)
at io.vertx.core.impl.future.FutureImpl.tryFail(FutureImpl.java:278)
at io.vertx.core.http.impl.HttpClientRequestBase.fail(HttpClientRequestBase.java:188)
at io.vertx.core.http.impl.HttpClientRequestBase.handleException(HttpClientRequestBase.java:183)
at io.vertx.core.http.impl.HttpClientRequestImpl.handleException(HttpClientRequestImpl.java:90)
at io.vertx.core.http.impl.Http1xClientConnection$StreamImpl.handleException(Http1xClientConnection.java:767)
at io.vertx.core.http.impl.Http1xClientConnection$StreamImpl.handleClosed(Http1xClientConnection.java:774)
at io.vertx.core.impl.ContextImpl.lambda$execute$7(ContextImpl.java:329)
...
We have no production stack trace for the endRequest path. The unit test below reproduces it.
To Reproduce
Steps to reproduce the behavior:
- Send a request with a streaming body (
Flux<ByteBuffer>orInputStream), so the body goes throughVertxRequestWriteSubscriber. - Let the connection close (or the response timeout fire) while the request is ending, after the stream has completed.
- The response future and the
end()future both fail. Whichever runs second callspromise.failon a completed promise and throws.
This is timing dependent and we have not reproduced it end to end. The unit test below reproduces it deterministically.
Code Snippet
A test in the style of VertxRequestWriteSubscriberTests. It drives the subscriber signals directly, because Flux.subscribe catches a throw from onComplete and routes it to onError, which hides the bug in a test. In production the throw comes from a Vert.x future listener (when end() fails after the write callback) and reaches the context exception handler.
@Test
public void endFailureAfterPromiseFailedExternallyIsNotThrown() {
RuntimeException externalError = new RuntimeException("Connection was closed");
Promise<HttpResponse> promise = Promise.promise();
Supplier<Future<Void>> end = () -> {
promise.fail(externalError);
return Future.failedFuture(new RuntimeException("End error"));
};
VertxRequestWriteSubscriber subscriber = new VertxRequestWriteSubscriber(IGNORED_EXCEPTION_HANDLER,
IGNORED_DRAIN_HANDLER, ignored -> Future.succeededFuture(), () -> false, IGNORED_RESET, end, promise, null,
Context.empty());
subscriber.onSubscribe(Operators.emptySubscription());
subscriber.onNext(ByteBuffer.wrap(DATA));
assertDoesNotThrow(subscriber::onComplete);
assertSame(externalError, promise.future().cause());
}We ran the equivalent against the main source (9970d5b) with Vert.x 4.5.34: onComplete throws IllegalStateException: Result is already complete. With promise::tryFail it does not throw, and the promise keeps the original cause.
Suggested fix:
end.get().onFailure(promise::tryFail);Like resetRequest, the losing error could also be attached as a suppressed exception if that is preferred.
Expected behavior
The first completion of the promise wins. Later failures are ignored or attached as suppressed exceptions, and no uncaught exception reaches the Vert.x context.
Screenshots
N/A
Setup (please complete the following information):
- OS: Linux (container on Kubernetes)
- IDE: N/A
- Library/Libraries: com.azure:azure-core-http-vertx:1.2.0, com.azure:azure-core:1.60.0, com.azure:azure-storage-file-share:12.32.0, io.vertx:vertx-core:4.5.34
- Java version: 25
- App Server/Environment: GraalVM native image
- Frameworks: Quarkus 3.40.1
Additional context
The upload or request itself still fails or succeeds correctly. The issue is the extra uncaught exception, which shows up as an error in logs and error trackers and hides the real cause.
Information Checklist
- Bug Description Added
- Repro Steps Added
- Setup information Added
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
github-actions commentedon Oct 23, 2026
🎯 Agentic Issue Triage
Summary:
VertxRequestWriteSubscriber.endRequestusespromise::failon a shared promise that other paths may already have completed, causing an uncaughtIllegalStateException: Result is already complete. The author suggestspromise::tryFail.📋 Issue Details
Package:
com.azure:azure-core-http-vertx(1.2.0 reported; 1.3.0-beta.1 on main)Affected API:
VertxRequestWriteSubscriber.endRequestScenarios:
end()future both fail, and the secondpromise.failthrowsRoot ask: Change to
tryFail(as inresetRequestand #50821).🔎 Debugging / Reproduction Notes
The report includes a deterministic unit test. The fix looks small and consistent with earlier hardening (#43399 / #43402, #50821). The deprecation CSV could not be fetched in this run. The package is in active development in the repo (1.3.0-beta.1), so it is assumed not deprecated.
Suggested investigation steps:
endRequeststill usespromise::failon main.tryFailand consider attaching the suppressed exception.VertxRequestWriteSubscriberTestsand update the CHANGELOG.🏷️ Label Confidence
Client— data-plane SDK code behaviorAzure.Core— package is under sdk/corecustomer-reportedandquestionwere added.👥 Owner Routing
# ServiceLabel: %Azure.Core(line 56) — all labels matched@alzimmermsftneeds-team-attention%Mgmt(line 945) did not match because the category is Client;%Azure.Core.V2and%Azure.Core.AMQPare different labels.github-actions commentedon Oct 23, 2026
Thank you for your feedback. Tagging and routing to the team member(s) best able to assist.