quarkusio/quarkus · error · IllegalArgumentException
@ExactlyOnce on method ${methodName} requires both @Incoming
Error message
@ExactlyOnce on method ${methodName} requires both @Incoming and @Outgoing annotations What it means
The Kafka connector's @ExactlyOnce processing mode requires a method annotated @ExactlyOnce to be a full processing step: it must have both @Incoming and @Outgoing so the framework can coordinate commit of incoming offsets with publishing outgoing records. A method missing either annotation fails deployment with IllegalArgumentException.
Source
Thrown at extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java:150
CombinedIndexBuildItem combinedIndex,
List<ConnectorManagedChannelBuildItem> channelsManagedByConnectors,
BuildProducer<RunTimeConfigurationDefaultBuildItem> defaultConfigProducer,
BuildProducer<InjectedEmitterBuildItem> emitters) {
DefaultSerdeDiscoveryState discoveryState = new DefaultSerdeDiscoveryState(combinedIndex.getIndex());
for (AnnotationInstance annotation : combinedIndex.getIndex().getAnnotations(DotNames.EXACTLY_ONCE)) {
if (annotation.target().kind() != AnnotationTarget.Kind.METHOD) {
continue;
}
MethodInfo method = annotation.target().asMethod();
String methodName = method.declaringClass().name() + "#" + method.name();
AnnotationInstance incoming = method.annotation(DotNames.INCOMING);
AnnotationInstance outgoing = method.annotation(DotNames.OUTGOING);
if (incoming == null || outgoing == null) {
throw new IllegalArgumentException(
"@ExactlyOnce on method " + methodName + " requires both @Incoming and @Outgoing annotations");
}
if (method.parametersCount() == 0) {
throw new IllegalArgumentException(
"@ExactlyOnce on method " + methodName + " requires at least one parameter");
}
for (Type paramType : method.parameterTypes()) {
if (paramType.name().equals(DotNames.MESSAGE)) {
throw new IllegalArgumentException(
"@ExactlyOnce on method " + methodName
+ " does not support Message parameters, use payload types instead");
}
}
if (method.returnType().name().equals(DotNames.VOID)
|| method.returnType().name().equals(VOID_BOXED)) {View on GitHub (pinned to e1c734241f)
Solutions
- Add both @Incoming and @Outgoing to the @ExactlyOnce method.
- If only consuming, remove @ExactlyOnce and configure the connector's processing.garbage-collection / commit strategy instead.
- Use normal at-least-once semantics with `quarkus.messaging.kafka... processing.failsafe` if a pure consumer is intended.
Example fix
// before
@Incoming("in")
@ExactlyOnce
void consume(String m) { ... }
// after
@Incoming("in")
@Outgoing("out")
@ExactlyOnce
String process(String m) { return transform(m); } Defensive patterns
Strategy: validation
Validate before calling
fun validateExactlyOnce(methodAnnotations: Set<String>): Boolean =
"ExactlyOnce" !in methodAnnotations ||
("Incoming" in methodAnnotations && "Outgoing" in methodAnnotations) Prevention
- Remember @ExactlyOnce applies to processing (in+out) methods only.
- Review @ExactlyOnce usage in code review to catch misplaced annotations.
- Read the Kafka connector docs' exactly-once section before adopting it.
When it happens
Trigger: Annotating a method with @ExactlyOnce but only @Incoming, only @Outgoing, or neither.
Common situations: Copy-pasting @ExactlyOnce onto a consumer-only method; misunderstanding that exactly-once applies to the processing chain, not a plain consumer.
Related errors
- @ExactlyOnce on method ${methodName} requires at least one p
- @ExactlyOnce on method ${methodName} does not support Messag
- @${annotation} value cannot be blank. Offending method is: $
- Emitter configuration for channel `${channel}` is different
- @ExactlyOnce on method ${methodName} must return a value to
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/891b5ed69c655770.
Report an issue: GitHub.