quarkusio/quarkus · error · java.lang.IllegalArgumentException
onCancel was already called
Error message
onCancel was already called
What it means
This IllegalArgumentException is thrown by the client's Multi invoker when a second onCancel callback is registered after one was already provided for a subscription (e.g. an SSE/LongStream Multi). The library stores at most one cancel callback per subscription; registering a second means the developer has subscribed the same Multi instance twice or set the callback twice.
Source
Thrown at independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java:118
}
private void cancel() {
Runnable action = onCancel.getAndSet(CLEARED);
if (action != null && action != CLEARED) {
action.run();
}
}
public void onCancel(Runnable onCancel) {
if (this.onCancel.compareAndSet(null, onCancel)) {
// this was a first set
} else if (this.onCancel.get() == CLEARED) {
// already cleared
if (onCancel != null)
onCancel.run();
} else {
// it was already set
throw new IllegalArgumentException("onCancel was already called");
}
}
}
@Override
public <R> Multi<R> method(String name, Entity<?> entity, GenericType<R> responseType) {
return method(name, entity, responseType, false);
}
public <R> Multi<R> method(String name, Entity<?> entity, GenericType<R> responseType,
boolean wrapAsRestMultiResponse) {
AsyncInvokerImpl invoker = (AsyncInvokerImpl) invocationBuilder.rx();
CompletableFuture<BasicRestResponse> restResponseFuture = wrapAsRestMultiResponse ? new CompletableFuture<>() : null;
// FIXME: backpressure setting?
Multi<R> multi = Multi.createFrom().emitter(emitter -> {
MultiRequest<R> multiRequest = new MultiRequest<>(emitter);
RestClientRequestContext restClientRequestContext = invoker.performRequestInternal(name, entity, responseType,
false);View on GitHub (pinned to e1c734241f)
Solutions
- Create a fresh Multi (call the client method again) for each subscription instead of reusing one instance
- Cancel/unsubscribe from the previous subscription before registering a new onCancel
- Refactor to use a single subscription and fan out results internally (e.g. Multi.createBy().concatenating or a shared broadcast)
Example fix
// before Multi<String> multi = client.sse(); multi.subscribe().with(...); multi.subscribe().with(...); // throws: onCancel was already called // after client.sse().subscribe().with(...); client.sse().subscribe().with(...); // new instance each call
Defensive patterns
Strategy: try-catch
Validate before calling
if (multi == null || alreadySubscribed.get()) {
throw new IllegalStateException("Cannot register onCancel: Multi already subscribed");
} Try / catch
try {
multi.subscribe().with(onItem, onFailure);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("onCancel was already called")) {
multi = client.sse(); // recreate and resubscribe
multi.subscribe().with(onItem, onFailure);
} else throw e;
} Prevention
- Never reuse a Multi instance for multiple subscriptions; call the client method again
- Track subscription state before registering callbacks
- Use AtomicBoolean guards around registration logic
When it happens
Trigger: Calling withOnCancel (via registerForSse) twice on the same MultiInvoker/Multi instance — typically by subscribing the same Multi more than once, or passing a new onCancel after it was already set and not cleared.
Common situations: Reusing a single Multi returned by restClient.sse()/LongStream across multiple subscriptions; re-subscribing after a previous subscription completed or was cancelled; accidentally calling the registration API directly twice in custom client wiring.
Related errors
- Delay must be > 0: <delay>
- Delay must be > 0: <reconnectDelay>
- Cannot read '<fieldName>' field from <object> of class <claz
- Was already open
- Invalid REST Client URL used: '<uri>'
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/f302ac8245a473e5.
Report an issue: GitHub.