Skip to content

Skip @PostFilter when method returns null#19321

Open
seonwooj0810 wants to merge 1 commit into
spring-projects:mainfrom
seonwooj0810:fix/gh-19280-postfilter-null-return
Open

Skip @PostFilter when method returns null#19321
seonwooj0810 wants to merge 1 commit into
spring-projects:mainfrom
seonwooj0810:fix/gh-19280-postfilter-null-return

Conversation

@seonwooj0810

Copy link
Copy Markdown

Closes gh-19280

Background

When a @PostFilter-annotated method returns null, the legacy @EnableGlobalMethodSecurity pipeline returned null to the caller. After migrating the same method to @EnableMethodSecurity, it now throws IllegalArgumentException: Filter target must be a collection, array, map or stream type, but was null. The issue suggests this is either a behavioural bug in the new interceptor or a documentation gap; this PR takes the first option and restores the legacy behaviour.

Root cause

PostFilterAuthorizationMethodInterceptor#invoke calls MethodSecurityExpressionHandler#filter unconditionally with whatever the intercepted method returned. DefaultMethodSecurityExpressionHandler#filter requires the filter target to be a Collection, array, Map, or Stream and otherwise throws IllegalArgumentException. null does not satisfy any of those instanceof checks (the array check is explicitly null-guarded), so the handler always throws when the method returns null.

The deprecated legacy implementation, ExpressionBasedPostInvocationAdvice#after, guards the call explicitly:

if (returnedObject != null) {
    returnedObject = this.expressionHandler.filter(returnedObject, postFilter, ctx);
}
else {
    this.logger.debug("Return object is null, filtering will be skipped");
}

That guard was not carried over to the new AuthorizationManager-based interceptor, which is what made the migration a behavioural breaking change for any service that returns null as a "nothing to filter" signal.

Change

PostFilterAuthorizationMethodInterceptor#invoke now short-circuits when the intercepted method returns null: it logs the same debug message as the legacy advice and returns null to the caller without consulting the expression handler. The MethodSecurityExpressionHandler#filter contract is intentionally left unchanged — semantically null is not a collection, so the handler should still reject it; the interceptor is the right layer to decide that @PostFilter over a null return is a no-op.

Tests

Added invokeWhenReturnedObjectIsNullThenSkipsFilteringAndReturnsNull in PostFilterAuthorizationMethodInterceptorTests covering the scenario from the issue: an intercepted invocation whose proceed() returns null now resolves to null instead of throwing. Before the fix this test fails with IllegalArgumentException from DefaultMethodSecurityExpressionHandler#filter; after, it passes.

./gradlew :spring-security-core:test --tests 'org.springframework.security.authorization.method.*' runs all 117 method-authorization tests green, so no other consumer of the interceptor is affected. :spring-security-core:checkstyleMain and :checkstyleTest also pass.

Verification done

  1. No in-flight PR / claim: gh pr list --repo spring-projects/spring-security --search "PostFilter null" and ... "PostFilterAuthorizationMethodInterceptor" returned no open PRs touching this code path; the issue thread has no "I'll work on this" claim.
  2. Code still on main: confirmed PostFilterAuthorizationMethodInterceptor#invoke on main (HEAD bf06d19225) still calls expressionHandler.filter without a null guard.
  3. Legacy implementation present: confirmed ExpressionBasedPostInvocationAdvice#after in access/src/main/java/... still has the explicit if (returnedObject != null) branch this PR mirrors.
  4. Behavioural impact: reproduced the IAE via the new regression test before applying the fix; after the fix the test passes and all sibling tests in the package remain green.

PostFilterAuthorizationMethodInterceptor#invoke called
MethodSecurityExpressionHandler#filter unconditionally with the value
returned by the intercepted method. DefaultMethodSecurityExpressionHandler#filter
requires the filter target to be a Collection, array, Map, or Stream and
throws IllegalArgumentException for any other value, including null. As
a result, any @PostFilter-annotated method that returns null under
@EnableMethodSecurity threw IllegalArgumentException instead of returning
null to the caller.

The legacy ExpressionBasedPostInvocationAdvice, used by the deprecated
@EnableGlobalMethodSecurity, explicitly skipped filtering when the
returned object was null and logged "Return object is null, filtering
will be skipped". The new interceptor introduced as part of the
AuthorizationManager-based method security pipeline did not carry that
guard over, which made the migration from @EnableGlobalMethodSecurity to
@EnableMethodSecurity a behavioural breaking change for callers relying
on null as a no-op signal (for example, a service that returns null when
there is nothing to filter).

Skip the filter call when the returned object is null and log the same
debug message as the legacy advice so that @PostFilter on null returns
preserves the documented behaviour across both APIs.

Closes spring-projectsgh-19280

Signed-off-by: seonwoo_jung <laborlawseon@kap.kr>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@PostFilter throws an IllegalArgumentException on null return after migrating to @EnableMethodSecurity

2 participants