spring-projects/spring-ai · error · ClassCastException

Expected Mono<Void> but got Mono<

Error message

Expected Mono<Void> but got Mono<

What it means

Async resource-list-changed callbacks may return Mono<Void>; at invocation time the library flattens the returned Mono and throws a ClassCastException if the emitted value is non-null, meaning the Mono was Mono<SomethingElse> rather than Mono<Void>. The message names the observed value's class.

Source

Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/changed/resource/AsyncMcpResourceListChangedMethodCallback.java:82

			// Build arguments for the method call
			Object[] args = this.buildArgs(this.method, null, updatedResources);

			// Invoke the method
			this.method.setAccessible(true);
			Object result = this.method.invoke(this.bean, args);

			// If the method returns a Mono, handle it
			if (result instanceof Mono) {
				// We need to handle the case where the Mono is not a Mono<Void>
				// This is expected by the test testInvalidMonoReturnType
				Mono<?> monoResult = (Mono<?>) result;

				// Convert the Mono to a Mono<Void> by checking the value
				// If the value is not null (i.e., not Void), throw a ClassCastException
				return monoResult.flatMap(value -> {
					if (value != null) {
						// This will be caught by the test testInvalidMonoReturnType
						throw new ClassCastException(
								"Expected Mono<Void> but got Mono<" + value.getClass().getName() + ">");
					}
					return Mono.empty();
				}).then();
			}
			// If the method returns void, return an empty Mono
			return Mono.empty();
		}
		catch (Exception e) {
			return Mono.error(new McpResourceListChangedConsumerMethodException(
					"Error invoking resource list changed consumer method: " + this.method.getName(), e));
		}
	}

	/**
	 * Validates that the method return type is compatible with the resource list changed
	 * consumer callback.
	 * @param method The method to validate

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Convert the reactive chain to Mono<Void> by appending .then() (or .then(Mono.empty())).
  2. Replace Mono.just(value) returns with Mono.empty().
  3. If a value must be produced, change the method to void return type and handle the value internally.

Example fix

// before
public Mono<String> onResourcesChanged(List<McpSchema.Resource> r) { return doWork(r); }
// after
public Mono<Void> onResourcesChanged(List<McpSchema.Resource> r) { return doWork(r).then(); }
Defensive patterns

Strategy: validation

Validate before calling

Method m = bean.getClass().getMethod("onResourcesChanged", List.class);
if (!Mono.class.isAssignableFrom(m.getReturnType())) throw new IllegalStateException("async callback must return Mono<Void>");

Type guard

static boolean returnsMonoVoid(Method m) {
    return Mono.class.isAssignableFrom(m.getReturnType());
}

Try / catch

try {
    callback.accept(resources).block();
} catch (ClassCastException e) {
    // Mono emitted a non-Void value; fix the method to end with .then()
    log.error("Async callback returned Mono<T> instead of Mono<Void>: {}", e.getMessage());
}

Prevention

When it happens

Trigger: An async callback method returns Mono<String>/Mono<Boolean>/Mono<List<...>>; the value emitted at runtime is non-null inside flatMap, triggering the ClassCastException.

Common situations: Returning Mono.just("done") or a service call that returns typed Monos instead of chaining .then() or Mono.empty() to convert to Mono<Void>.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/a483fb9632b32aa8. Report an issue: GitHub.