Skip to content

Avoid NullAway false positive in ContentCachingResponseWrapper.copyBodyToResponse - #37528

Open
vlsi wants to merge 1 commit into
spring-projects:mainfrom
vlsi:nullaway-content-length
Open

vlsi wants to merge 1 commit into
spring-projects:mainfrom
vlsi:nullaway-content-length

Conversation

@vlsi

@vlsi vlsi commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

This is not a bug fix. It rewrites one expression in ContentCachingResponseWrapper.copyBodyToResponse(boolean) so that an upcoming NullAway check does not report a false positive there.

uber/NullAway#2005 makes NullAway report a nullable boxed operand that is unboxed in a conditional expression of primitive type. With it, the NullAway CI job that builds Spring Framework against the snapshot fails on this line:

rawResponse.setContentLength(complete ? this.content.size() : this.contentLength);
[NullAway] unboxing of a @Nullable expression 'this.contentLength'

The unboxing is safe: the this.contentLength branch runs only when complete is false, and the enclosing if (complete || this.contentLength != null) has then established that the field is non-null. NullAway does not correlate a boolean with a null check (uber/NullAway#98), so it treats the field as nullable after that condition.

The rewrite tests the field in the conditional itself:

rawResponse.setContentLength(!complete && this.contentLength != null ?
		this.contentLength : this.content.size());

The result is the same for every input: complete still yields content.size(), and !complete still yields contentLength, which the outer if guarantees is non-null. The expression stays int-typed, so no Integer is allocated; a variant that boxed content.size() was rejected for that reason.

:spring-web:compileJava (which runs NullAway) and ContentCachingResponseWrapperTests pass. A model of this method in NullAway with #2005 applied reports the original form and not the rewritten one.

🤖 Generated with Claude Code

ContentCachingResponseWrapper.copyBodyToResponse(boolean) passed
`complete ? this.content.size() : this.contentLength` to
setContentLength(int). The unboxing of the nullable contentLength field
is safe: that branch runs only when complete is false, and the enclosing
`complete || this.contentLength != null` check has then established
that the field is non-null. NullAway does not correlate a boolean with
a null check, though, and the upcoming check for nullable operands
unboxed in a conditional expression reports the line:

    `[NullAway] unboxing of a @nullable expression 'this.contentLength'`

Test the field directly in the conditional so that NullAway sees the
null check. The result is the same for every input, and the expression
stays int-typed, so no Integer is allocated.

Link: uber/NullAway#2005
Link: uber/NullAway#98
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Oct 20, 2026
@sbrannen sbrannen added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Oct 21, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in: web Issues in web modules (web, webmvc, webflux, websocket) status: waiting-for-triage An issue we've not yet triaged or decided on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants