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

  1. Create a fresh Multi (call the client method again) for each subscription instead of reusing one instance
  2. Cancel/unsubscribe from the previous subscription before registering a new onCancel
  3. 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

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


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/f302ac8245a473e5. Report an issue: GitHub.