Skip to content

[#2931] - Rebuild expired Faces views during form replay without a preliminary GET - #2930

Merged
lprimak merged 3 commits into
apache:mainfrom
lprimak:simplify-faces-resubmit
Oct 27, 2026
Merged

lprimak merged 3 commits into
apache:mainfrom
lprimak:simplify-faces-resubmit

Conversation

@lprimak

@lprimak lprimak commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Replace the view-state GET and HTML scraping with FormResubmitViewHandler, using OmniFaces ComponentsLocal.buildView() to rebuild expired server-side views during form replay. Replays use a single POST; ordinary requests and client-side state saving are unchanged.

Fixes #2931


Following this checklist to help us incorporate your contribution quickly and easily:

  • Make sure there is a GitHub issue filed
    for the change (usually before you start working on it). Trivial changes like typos do not
    require a GitHub issue. Your pull request should address just this issue, without pulling in other changes.
  • Format the pull request title like [#XXX] - Fixes bug in SessionManager,
    where you replace #XXX with the appropriate GitHub issue. Best practice
    is to use the GitHub issue title in the pull request title and in the first line of the commit message.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • add fixes #XXX if merging the PR should close a related issue.
  • Run mvn verify to make sure basic checks pass. A more thorough check will be performed on your pull request automatically.
  • Committers: Make sure a milestone is set on the PR
  • Committers: Use "Squash and Merge" to combine all commits into one when merging a PR when appropriate.

Trivial changes like typos do not require a GitHub issue (javadoc, comments...).
In this case, just format the pull request title like [DOC] - Add javadoc in SessionManager.

If this is your first contribution, you have to read the Contribution Guidelines

If your pull request is about ~20 lines of code you don't need to sign an Individual Contributor License Agreement
if you are unsure please ask on the developers list.

To make clear that you license your contribution under the Apache License Version 2.0, January 2004
you have to acknowledge this by using the following check-box.

…r instead of scraping the view state via extra GET request during rebuilding of expired views during form data saving replay
@lprimak lprimak added the jakartaee Jakarta EE label Oct 26, 2026
@lprimak lprimak added this to the 3.0.2 milestone Oct 26, 2026
@lprimak lprimak changed the title enh: simplify code and process by implementing FormResubmitViewHandle… [#2931] - Rebuild expired Faces views during form replay without a preliminary GET Oct 26, 2026
@lprimak
lprimak requested a review from steinarb October 26, 2026 17:05
@lprimak lprimak self-assigned this Oct 26, 2026
@github-actions github-actions Bot added java Pull requests that update Java code tests xml labels Oct 26, 2026
@BalusC

BalusC commented Oct 26, 2026 •

Copy link
Copy Markdown

Nice. The gate looks right: the view is only rebuilt for a FormResubmitRequest, and that request only exists after the same-origin checks. So a forged or cross-site postback cannot trigger a rebuild.

A few points.

M1. On MyFaces, a rebuilt Ajax view is not re-rendered with @all

replaySavedForm still computes isStateless with isJSFStatefulForm, and that still uses the Mojarra pattern -?\d+:-?\d+. The MyFaces server-side token is Base64, so it never matches, and the form counts as stateless. The view handler rebuilds the view anyway, because it does not check isStateless. But AjaxReplay.forForm(isPartial, true) downgrades RENDER_ALL to PASS_THROUGH. So on MyFaces an Ajax replay renders only the form's own targets, on a view that was built from scratch. The other forms on the page keep their expired view state. I did not run this on MyFaces; it follows from the code.

Fix: in isJSFStatefulForm, treat any non-blank view state other than the literal stateless as stateful. Both Mojarra and MyFaces use literal string "stateless" for a stateless view. Add a unit test with a MyFaces-style Base64 token.

m2. The view is built while Faces events are off

Restore View calls setProcessingEvents(false) before restoreView, in Mojarra and in MyFaces. So ComponentsLocal.buildView runs without PostAddToViewEvent. Things like f:event type="postAddToView" and the relocation of h:outputScript target="head" depend on it. Mojarra turns events on for its own rebuild of a stateless view (FaceletViewHandlingStrategy). This matters most when the response renders @all.

Fix: call context.setProcessingEvents(true) around buildView, and restore the previous value in a finally.

m3. The reason in the noJSFAjaxRequests javadoc no longer holds

It says the Ajax fields are only kept for stateless views, because there they cannot fail view state restoration. With the view handler, a lost stateful view is rebuilt, so it cannot fail restoration either. The stateless/stateful split still changes behavior: it decides whether the replay executes the whole form or only the partial execute.

Fix: decide if the split is still wanted. If yes, rewrite the javadoc with the reason that still holds. If no, keep the Ajax fields for both.

n4. Unrelated change in FormSupportTest

parseFormDataHonoursCharset replaces the ö escapes with a literal ö. This is not related to #2931, and the test now depends on the source file encoding.

Fix: revert those two lines.

n5. @SneakyThrows(IOException.class) on restoreView

This throws a checked exception from a method that does not declare it. Wrapping it in a FacesException is the usual way, and then the application's ExceptionHandler handles it normally.

Fix: catch the IOException and throw new FacesException(e). Update buildFailurePropagatesToReplayFallback to expect that.

Possible follow-up: drop the in-place replay

With this view handler in place, ShiroFilter does not need to read the body and dispatch it a second time for an expired same-origin form. It can mark the request in createSubject (after the same-origin check), let it continue down the normal filter chain, and let the view handler rebuild the view for marked requests. That would:

  • keep the request body readable for non-Faces endpoints, such as a servlet that calls getInputStream() (today it gets a redirect, because the body was already consumed);
  • run the remaining filters in the chain once, in the normal order;
  • remove the unbounded read of the request body into memory.

The after-login replay would stay as it is.

🤖 Generated with Claude Opus 5.5

…d improving view rebuilding during expired session handling

addresses all valid review comments
@lprimak

lprimak commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the review, @BalusC
All have been addressed, code simplified, except n4 and n5 which I don't agree with (again, Claude tries to "revert to mean" there)

@lprimak
lprimak merged commit 12d194b into apache:main Oct 27, 2026
19 checks passed
@lprimak
lprimak deleted the simplify-faces-resubmit branch October 27, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jakartaee Jakarta EE java Pull requests that update Java code tests xml

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement] Rebuild expired Faces views during form replay without a preliminary GET

3 participants