spring-projects/spring-framework · error · AopInvocationException

Null return value from advice does not match primitive…

Error message

Null return value from advice does not match primitive return type for: {}

What it means

Thrown at invocation time by JdkDynamicAopProxy.invoke when the proxy returned null for a method whose declared return type is a primitive (int, boolean, etc.). Because a primitive cannot hold null, the runtime detects the mismatch and aborts with AopInvocationException. This almost always points to an around-advice/MethodInterceptor that short-circuits proceed() and returns null.

Solutions

  1. Fix the advice: when intercepting a primitive-returning method, return the boxed default (0 for int, false for boolean) or ensure proceed() is actually called.
  2. If the advice legitimately cannot compute a value, change the proxied method's return type to the wrapper type (Integer instead of int) so null is valid.
  3. Make the fallback branch in the interceptor return the appropriate primitive default instead of null.

Example fix

// before
public class CacheInterceptor implements MethodInterceptor {
  public Object invoke(MethodInvocation i) throws Throwable {
    if (cacheMiss) return null; // breaks int return
    return i.proceed();
  }
}

// after
public class CacheInterceptor implements MethodInterceptor {
  public Object invoke(MethodInvocation i) throws Throwable {
    if (cacheMiss) {
      Class<?> rt = i.getMethod().getReturnType();
      return rt.isPrimitive() ? defaultFor(rt) : null;
    }
    return i.proceed();
  }
}
Defensive patterns

Strategy: validation

Validate before calling

// In an around-advice, before returning null:
Class<?> rt = invocation.getMethod().getReturnType();
if (rt.isPrimitive()) {
  // cannot return null; return the boxed default
  return defaultPrimitive(rt);
}
return null;

static Object defaultPrimitive(Class<?> p) {
  if (p == boolean.class) return false;
  if (p == byte.class) return (byte) 0;
  if (p == short.class) return (short) 0;
  if (p == int.class) return 0;
  if (p == long.class) return 0L;
  if (p == float.class) return 0f;
  if (p == double.class) return 0d;
  if (p == char.class) return '\0';
  throw new IllegalStateException();
}

Type guard

static boolean returnsPrimitive(Method m) { return m.getReturnType().isPrimitive(); }

Try / catch

try { return proxy.someIntMethod(); }
catch (AopInvocationException e) {
  if (e.getMessage().contains("primitive return type")) { /* fix advice to not return null */ }
  throw e;
}

Prevention

When it happens

Trigger: A proxied method 'public int compute()' is intercepted by an advice (e.g. caching, fallback, or mis-written MethodInterceptor) that returns null without invoking proceed(), or whose proceed() returned null. Line 235-238 fires when retVal == null && returnType != void.class && returnType.isPrimitive().

Common situations: A caching interceptor returns null on a miss for a primitive-returning method; a circuit-breaker/fallback advice returns null; a @Around advice written to 'return null' on a guard path; mocking the wrong return type in tests of an AOP-proxied bean.

Related errors


AI-assisted analysis of spring-projects/spring-framework@69bf83ad71 (2026-08-09). Data as JSON: /api/errors/916f7cf76cb4b841. Report an issue: GitHub.

Appendix: source

Thrown at spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java:236

				// We need to create a method invocation...
				MethodInvocation invocation =
						new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				// Proceed to the joinpoint through the interceptor chain.
				retVal = invocation.proceed();
			}

			// Massage return value if necessary.
			Class<?> returnType = method.getReturnType();
			if (retVal != null && retVal == target &&
					returnType != Object.class && returnType.isInstance(proxy) &&
					!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
				// Special case: it returned "this" and the return type of the method
				// is type-compatible. Note that we can't help if the target sets
				// a reference to itself in another returned object.
				retVal = proxy;
			}
			else if (retVal == null && returnType != void.class && returnType.isPrimitive()) {
				throw new AopInvocationException(
						"Null return value from advice does not match primitive return type for: " + method);
			}
			if (COROUTINES_REACTOR_PRESENT && KotlinDetector.isSuspendingFunction(method)) {
				return COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()) ?
						CoroutinesUtils.asFlow(retVal) : CoroutinesUtils.awaitSingleOrNull(retVal, args[args.length - 1]);
			}
			return retVal;
		}
		finally {
			if (target != null && !targetSource.isStatic()) {
				// Must have come from TargetSource.
				targetSource.releaseTarget(target);
			}
			if (setProxyContext) {
				// Restore old proxy.
				AopContext.setCurrentProxy(oldProxy);
			}
		}

View on GitHub (pinned to 69bf83ad71)