Skip to content

WW-5789 fix(rest): keep params-bound values when the body handler binds nothing - #2050

Merged
lukaszlenart merged 1 commit into
mainfrom
WW-5789-rest-form-handlers-keep-bound-values
Oct 23, 2026
Merged

lukaszlenart merged 1 commit into
mainfrom
WW-5789-rest-form-handlers-keep-bound-values

Conversation

@lukaszlenart

@lukaszlenart lukaszlenart commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Fixes WW-5789

What was wrong

A ModelDriven action in a package extending rest-default lost form-bound values on every model property that has a non-null constructor default, while JSON bodies and struts.parameters.requireAnnotations=false both worked. Regression from 7.2.0.

Since WW-5626, ContentTypeInterceptor runs a two-phase copy for every handler that is not an AuthorizationAwareContentTypeHandler when requireAnnotations=true: build a fresh instance via the no-arg constructor, let the handler fill it, copy every non-null property onto the real target. FormUrlEncodedHandler, MultipartFormDataHandler and HtmlHandler have a no-op toObject, so the fresh instance carried only constructor defaults, and those were copied over what params had bound one interceptor earlier. Null defaults are skipped by the copy, which is exactly the reporter's "unless it is null by default".

Change

  • The three no-op handlers declare AuthorizationAwareContentTypeHandler, so the interceptor takes the direct path for them. They bind nothing, so there is nothing to authorize after the fact.
  • The marker's Javadoc admits the no-op case and states that a subclass overriding toObject to deserialize inherits the declaration and must honor the context itself.
  • ContentTypeInterceptorIntegrationTest: three tests on a ModelDriven fixture with a constructor default, one per handler. Each failed with expected:<[bound-by-params]> but was:<[new]> before the change.

Left out on purpose

The two-phase copy itself still cannot tell "the body said X" from "the constructor said X". A handler that stays on that path (XStreamHandler, which is opt-in, and any third-party handler) still copies a constructor default over a params-bound or prepare()-loaded value whenever the body omits that property. Fixing that means comparing against a pristine fresh instance inside copyAuthorizedProperties, which changes the semantics of the authorization copy. Tracked as WW-5790.

Verification

  • plugins/rest suite with core compiled from the branch: 226 tests, 0 failures.
  • Security review of the diff: no findings. The change only removes a spurious write; no request body reaches a setter it could not reach before.

🤖 Generated with Claude Code

…ds nothing

Since WW-5626 (7.2.0), ContentTypeInterceptor runs a two-phase copy for every
handler that is not AuthorizationAwareContentTypeHandler when
struts.parameters.requireAnnotations=true: a fresh instance is built with the
no-arg constructor, the handler fills it, and every non-null property is copied
onto the real target. FormUrlEncodedHandler, MultipartFormDataHandler and
HtmlHandler never touch the body, so the "fresh" instance holds only
constructor defaults, and those overwrote the values ParametersInterceptor had
bound one step earlier. Null defaults were skipped, which is why the report
reads "works unless the field is null by default".

The three handlers bind nothing, so they now declare the marker and take the
direct path. The marker Javadoc admits that case and names the subclass caveat.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
@lukaszlenart
lukaszlenart marked this pull request as ready for review October 22, 2026 20:01
@sonarqubecloud

Copy link
Copy Markdown

@lukaszlenart
lukaszlenart merged commit 8f3b4ab into main Oct 23, 2026
8 checks passed
@lukaszlenart
lukaszlenart deleted the WW-5789-rest-form-handlers-keep-bound-values branch October 23, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant