Flash Scope and Post-Redirect-Get
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11) with Eclipse Mojarra
4.1.17 and Apache MyFaces Core 4.1.4, Jakarta Servlet 6.1 and Java 17 or later (the examples were compiled with
--release 21). The flash exists since JSF 2.0, so everything below also applies to Faces 4.0, but only 4.1 was run. The
code in this page was deployed as an exploded Bookshelf WAR on Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and
once with MyFaces, and driven with curl (every request, redirect and cookie shown was observed on 2026-10-08, by
following the Location header by hand). Claims that were not run are marked as such.
A browser form is submitted with an HTTP POST. If the response to that POST is the confirmation page itself, the browser’s
address bar still shows the POST target, and pressing reload, or the back and forward buttons, offers to send the form
again. The cure is the Post/Redirect/Get pattern (PRG): after the POST the server answers with a redirect, and the browser
fetches the result with a GET. A redirect creates a new request, and a new request has a new FacesContext, new
request-scoped beans and an empty message list. Whatever the confirmation page needs from the POST has to travel across the
redirect, and the flash scope is Faces' answer to that.
The Problem: Rendering a Result from a POST
The Bookshelf review form posts a review. The simplest action saves it, queues a confirmation message and returns null,
which re-renders the same view in the POST response:
/** Without Post/Redirect/Get: the POST response itself shows the confirmation. */
public String submitNaive() {
reviews.save(new Review(bookTitle, rating, comment));
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Thanks for your review.", null));
return null;
}
Source: Jakarta Faces 4.1 specification, 7.4.2 Default NavigationHandler Algorithm and Jakarta EE Tutorial: Navigation Model
It works, and it has two defects that PRG removes:
-
Resubmission. The browser’s current page is the result of a POST. Reload repeats it. Sending the same valid form twice through this action stored two reviews: the page showing
Reviews stored so farread 1, then 2 (Mojarra and MyFaces alike). With a real database this is a duplicate order, a second payment or a second e-mail. -
Wrong URL. The address bar shows the page that received the POST, not the page that shows the result, so the result cannot be bookmarked, and the back button leads to a "confirm form resubmission" dialog.
A redirect after a successful POST gives a GET for the result page: reload now repeats a harmless read. In lifecycle terms
(see the phases) the action runs in Invoke Application, the
navigation handler turns the outcome into a redirect and completes the response, so Render Response never runs for the POST
(observed with a PhaseListener: the POST’s last phase was Invoke Application, on both implementations). The browser
then starts a second request, which goes through the lifecycle on its own.
The pattern in one picture:
Source: Jakarta Faces 4.1 API: Flash
The Flash: What It Is
The API documentation describes jakarta.faces.context.Flash as a way to pass temporary objects between the views
generated by the Faces lifecycle: anything placed in the flash is exposed to the next view the same user session
encounters and then cleared out. The concept comes from Ruby on Rails. Three properties matter:
-
It is a
Map<String, Object>.Flashis an abstract class that implementsMap, soput,get,containsKeyandremoveall work. -
It belongs to the user’s session, not to a browser tab, and must be thread safe. The values live on the server; the browser only receives a cookie that identifies the flash (see Mojarra and MyFaces). "The next view" therefore means the next request of that session, whichever tab sends it. Where the server keeps the values depends on the implementation: MyFaces 4.1.4 keeps them in the HTTP session, so they move with a replicated session, whereas Mojarra 4.1.17 keeps them in an application-wide store that it finds through the cookie, so on a second node the entry is not there even when the session was replicated (observed with two Tomcat nodes, see the deployment and clustering page).
-
It looks at two logical maps. The documentation says that during the execute portion of the lifecycle the flash reads the map written by the previous traversal, and during the render portion it writes the map for the next one. In practice, a value you
putwhile handling request N can be read in request N+1, in the action and in the rendered page alike.
You reach the flash in three ways, all of them observed to work on both implementations:
| Access | Where it fits |
|---|---|
|
Facelets pages (the read-only |
|
Any Java code that has the |
|
CDI beans. The specification lists |
The members of Flash beyond Map:
| Member | Meaning |
|---|---|
|
Stores a value for the next traversal. A |
|
Stores a value readable in the current traversal (an alias for putting it in the request map). In EL: |
|
Promotes a value to the flash so that it is available on the next traversal as well. In EL: |
|
Whether the |
|
Declares that the next request of the session will be a redirect. The navigation handler sets it for you (specification 7.4.2); you need it only when you issue a redirect yourself. |
|
Called by the lifecycle around every phase so the implementation can maintain the two maps. Application code never calls them; they matter if you write a |
Source: Jakarta Faces 4.1 API: Flash
Post-Redirect-Get End to End
The complete Bookshelf round trip has four parts: the form, the action that saves and redirects, the target view that shows the result, and a guard for people who open the target directly.
The form view (review.xhtml):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<h:head>
<title>Review a book</title>
</h:head>
<h:body>
<h:messages id="messages" globalOnly="true"/>
<h:form id="review">
<h:outputLabel for="title" value="Book"/>
<h:inputText id="title" value="#{reviewFormBean.bookTitle}" required="true"/>
<h:message for="title"/>
<h:outputLabel for="rating" value="Rating (1-5)"/>
<h:inputText id="rating" value="#{reviewFormBean.rating}">
<f:validateLongRange minimum="1" maximum="5"/>
</h:inputText>
<h:message for="rating"/>
<h:outputLabel for="comment" value="Comment"/>
<h:inputTextarea id="comment" value="#{reviewFormBean.comment}"/>
<h:commandButton id="send" value="Send" action="#{reviewFormBean.submit}"/>
<h:commandButton id="sendByRule" value="Send (rule)" action="#{reviewFormBean.submitByRule}"/>
<h:commandButton id="show" value="Show" action="#{reviewFormBean.show}"/>
<h:commandButton id="leave" value="Leave" action="#{reviewLeaveBean.leave}" immediate="true"/>
<h:commandButton id="sendNaive" value="Send (no redirect)" action="#{reviewFormBean.submitNaive}"/>
</h:form>
<p>Reviews stored so far: #{reviewDoneBean.total}</p>
</h:body>
</html>
The action (the same bean as before, now with the redirecting method). It saves the review, queues a global message, asks
the flash to keep it, hands the review itself to the next view and returns an outcome with faces-redirect=true:
package org.example.bookshelf.web;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.Flash;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.example.bookshelf.model.Review;
import org.example.bookshelf.service.ReviewService;
@Named
@RequestScoped
public class ReviewFormBean {
@Inject
private ReviewService reviews;
@Inject
private FacesContext facesContext;
@Inject
private Flash flash;
private String bookTitle;
private int rating = 5;
private String comment;
/** Without Post/Redirect/Get: the POST response itself shows the confirmation. */
public String submitNaive() {
reviews.save(new Review(bookTitle, rating, comment));
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Thanks for your review.", null));
return null;
}
/** Post/Redirect/Get: save, queue a message, hand the review to the next view, redirect. */
public String submit() {
Review review = new Review(bookTitle, rating, comment);
reviews.save(review);
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Thanks for your review.", null));
flash.setKeepMessages(true);
flash.put("lastReview", review);
return "review-done?faces-redirect=true";
}
/** Same, but the outcome is resolved by a navigation rule with a redirect element. */
public String submitByRule() {
Review review = new Review(bookTitle, rating, comment);
reviews.save(review);
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Thanks for your review.", null));
flash.setKeepMessages(true);
flash.put("lastReview", review);
return "reviewed";
}
/** Redirect to the page of the reviewed book, passing the title as a view parameter. */
public String show() {
return "show";
}
public String getBookTitle() { return bookTitle; }
public void setBookTitle(String bookTitle) { this.bookTitle = bookTitle; }
public int getRating() { return rating; }
public void setRating(int rating) { this.rating = rating; }
public String getComment() { return comment; }
public void setComment(String comment) { this.comment = comment; }
}
The target view (review-done.xhtml) reads the review from #{flash.lastReview} and shows the kept message with
h:messages. Its f:viewAction runs in Invoke Application of the GET request and protects the page:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<f:metadata>
<f:viewAction action="#{reviewDoneBean.guard}"/>
</f:metadata>
<h:head>
<title>Review saved</title>
</h:head>
<h:body>
<h:messages id="messages" globalOnly="true"/>
<p>Your review of #{flash.lastReview.bookTitle}: #{flash.lastReview.rating}/5</p>
<h:link outcome="review" value="Review another book"/>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.Flash;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.example.bookshelf.model.Review;
import org.example.bookshelf.service.ReviewService;
@Named
@RequestScoped
public class ReviewDoneBean {
@Inject
private FacesContext facesContext;
@Inject
private Flash flash;
@Inject
private ReviewService reviews;
/** f:viewAction: visitors who did not come from the form go back to it. */
public String guard() {
if (flash.containsKey("lastReview")) {
return null;
}
// MyFaces 4.1.4 stores the queued message in the session: make sure one exists
facesContext.getExternalContext().getSession(true);
facesContext.addMessage(null, new FacesMessage("Nothing to show: submit the form first."));
return "review?faces-redirect=true";
}
public Review getLastReview() {
return (Review) flash.get("lastReview");
}
private String title;
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public int getTotal() {
return reviews.count();
}
}
Source: Jakarta Faces 4.1 API: Flash, VDL: f:viewAction and VDL: h:messages
What was observed when this was driven with curl, identically on Mojarra and MyFaces:
| Step | Request | Result |
|---|---|---|
1 |
POST |
|
2 |
GET |
|
3 |
GET |
The guard found no |
4 |
POST with the rating 9 |
|
Two details of the guard are worth copying. First, f:viewAction runs on the initial GET request because the view has
metadata, so a visitor who reaches review-done.xhtml with an empty flash is sent back to the form instead of seeing a
page with blanks. Second, the specification says that when a navigation happens while a UIViewAction is being
broadcast, the runtime calls getFlash().setKeepMessages(true) itself (7.4.2). That is why the guard’s message reached
review.xhtml although the guard never calls setKeepMessages. This was observed on both implementations, with one
MyFaces exception described under Pitfalls.
Three Ways to Redirect
The flash only needs to know that the next request is a redirect, and the navigation handler tells it. There are three ways to get a redirect:
Implicit navigation with faces-redirect=true. The outcome string of the action (or of a h:link or h:button) is
a view id with a query string. The example above used "review-done?faces-redirect=true". The specification defines the
parameter and calls getFlash().setRedirect(true) before it sends the redirect and completes the response.
A navigation case with a <redirect/> element. The outcome stays a plain string and the redirect is configured in
faces-config.xml, so the bean does not know how its result is displayed:
<navigation-case>
<from-outcome>reviewed</from-outcome>
<to-view-id>/review-done.xhtml</to-view-id>
<redirect/>
</navigation-case>
The reviewed rule behaves exactly like the implicit outcome (the review was shown on the other side on both
implementations):
/** Same, but the outcome is resolved by a navigation rule with a redirect element. */
public String submitByRule() {
Review review = new Review(bookTitle, rating, comment);
reviews.save(review);
facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Thanks for your review.", null));
flash.setKeepMessages(true);
flash.put("lastReview", review);
return "reviewed";
}
ExternalContext.redirect(String). A hand-made redirect, for example to an external site or from code that has no
outcome. This bypasses the navigation handler, so nobody calls setRedirect(true). On Mojarra the flash survived such a
redirect anyway; on MyFaces the value was lost until flash.setRedirect(true) was called before redirect(...). To be
portable always call it:
/** A hand-made redirect: the navigation handler is bypassed, so tell the flash. */
public void leave() throws IOException {
flash.put("lastReview", new Review("Dune", 5, null));
flash.setRedirect(true);
ExternalContext external = facesContext.getExternalContext();
external.redirect(external.getRequestContextPath() + "/review-keep.xhtml");
}
Source: Jakarta Faces 4.1 API: ExternalContext, Jakarta EE Tutorial: Configuring Navigation Rules and Jakarta Faces 4.1 specification, 7.4.2
Both implementations answered the redirect with the status 302 Found. HTTP also defines 303 See Other, the status
whose meaning is "fetch the result with a GET"; browsers follow a 302 that answers a POST with a GET as well, which is
why the pattern works with either status (general HTTP behaviour, not tested here beyond curl).
The same applies to the full navigation story, which is a page of its own: outcomes, conditional navigation and
includeViewParams are covered by the navigation page (see its section on state across a redirect).
Messages Across a Redirect
Messages are queued on the FacesContext of one request, so they die with it. A redirect ends the request, and the
messages queued before it are gone by the time the GET arrives. Calling flash.setKeepMessages(true) before the
redirect saves them into the flash and restores them on the next request.
Observed on both implementations, with the same action once with and once without the call:
-
With
setKeepMessages(true): the redirected page showedSaved.inh:messages. -
Without it: the redirected page showed no message, although the action had queued one.
-
A reload of the redirected page showed no message: kept messages are consumed by the first request that follows.
Advice that follows from this:
-
Queue global messages (client id
null) when a redirect follows. The next view normally contains other components, so a message addressed to a component id of the form would have no matchingh:messagethere (a design argument, not something this page ran). -
Show them with
h:messages globalOnly="true"in a place that every target view shares (typically the template; see Messages and Internationalization). -
Keep validation messages out of this mechanism. When validation fails there is no redirect (step 4 above), so the usual
h:messagenext to the field is the right tool.
The API documentation also shows the EL spelling of the same switch, <c:set target="#{flash}" property="keepMessages"
value="true"/>, for pages that have no action (not run here).
How Long a Flash Entry Lives
An entry lives until the next request of the session has finished, then it is removed. Entries do not survive two requests unless they are kept. Observed with the redirected page of the example:
-
The first GET after the redirect showed the review; the second GET (reload) showed nothing. On the first GET the value was available in the action, in EL and in the rendered page.
-
A GET of a different page between the redirect and the intended target consumed the value, so the intended target then found nothing (both implementations). The flash is per session, not per browser tab: with two tabs open, whichever tab makes the next request wins. A second tab, or a request for any other page, can therefore take the value. Keep the flash for short, immediate hand-offs and give the target page a fallback (the guard above).
-
An entry put during a request that did not redirect (the action returned a normal outcome and the next view was shown by a forward) was visible in the page of that very response and then disappeared; a following GET found nothing. Do not rely on the flash for forwards.
To make a value survive one more request, keep it. In EL, reading it through flash.keep promotes it every time it is read:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html">
<h:head>
<title>Review saved</title>
</h:head>
<h:body>
<p>Your review of #{flash.keep.lastReview.bookTitle}</p>
<h:link outcome="review-keep" value="Reload"/>
</h:body>
</html>
Or in Java, for example from a preRenderView listener, when the page cannot be written with flash.keep:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<f:metadata>
<f:event type="preRenderView" listener="#{reviewLeaveBean.keepForNextRequest}"/>
</f:metadata>
<h:head>
<title>Review saved</title>
</h:head>
<h:body>
<p>Your review of #{flash.lastReview.bookTitle}</p>
</h:body>
</html>
/** Called before the page renders: keep the value for one more request. */
public void keepForNextRequest() {
flash.keep("lastReview");
}
Both forms were run: after the redirect, three consecutive GETs of either page still showed the review on both
implementations. The Javadoc of keep talks about values stored with putNow or in the request map; keeping a value
that came from the previous request also worked in practice, but the documentation does not promise it, so treat the
re-keep as an implementation behaviour. Keep entries sparingly: each kept value stays on the server for as long as the
user keeps requesting pages that keep it.
Links and Buttons That Issue a GET
h:link and h:button render ordinary links that the browser follows with a GET, so there is no action in which to call
flash.put. Pass what the next view needs as request parameters instead (f:param), and read them with f:viewParam.
The Flash documentation shows the pattern with flash.now: the view parameter writes into #{flash.now.picked}, which
is the request map, so it is readable by the beans and the page of that request.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<f:metadata>
<f:viewParam name="pick" value="#{flash.now.picked}"/>
</f:metadata>
<h:head>
<title>Picked author</title>
</h:head>
<h:body>
<p>Picked: #{flash.now.picked}</p>
<h:link outcome="review-link" value="Pick Emma">
<f:param name="pick" value="Emma"/>
</h:link>
</h:body>
</html>
Observed: opening review-link.xhtml?pick=Emma showed Picked: Emma; the link rendered the parameter as
?pick=Emma; a following request without the parameter showed Picked: with nothing (both implementations). The
documentation’s own example then reads the value as #{flash.picked} (without now) on the same page. That did not
show the value in this run on either implementation: flash.now.picked did. Use flash.now for reading in the same
request.
Source: Jakarta Faces 4.1 API: Flash, VDL: h:link, VDL: h:button and VDL: f:viewParam
Flash or Query Parameters?
The flash is the right carrier for one-shot data: a confirmation message, the object that was just created. It is the wrong carrier for identity. Data that says which resource the next page shows (a book id, a search term) belongs in the URL, because a URL can be bookmarked, shared and reloaded, and the flash cannot:
| Flash | Query parameter (f:viewParam) |
|
|---|---|---|
Survives reload |
No, unless kept |
Yes |
Bookmarkable and shareable |
No |
Yes |
Visible to the user |
No (stays on the server) |
Yes, so it must be validated like any input |
Size and type |
Any object the flash holds (keep it small and |
Strings, converted by Faces |
Good for |
Confirmations, the just-created object |
Which book, which page, which filter |
A good PRG often uses both: the identity as a parameter and the message through the flash. A navigation rule can build the query string from an expression:
<navigation-case>
<from-outcome>show</from-outcome>
<to-view-id>/review-param.xhtml</to-view-id>
<redirect>
<view-param>
<name>title</name>
<value>#{reviewFormBean.bookTitle}</value>
</view-param>
</redirect>
</navigation-case>
The rule ran on both implementations: for the title A & B the redirect went to /review-param.xhtml?title=A%26+B+
and the target page read the original title back.
Do not build the query string by concatenating user input into the outcome, as in
"review-param?faces-redirect=true&title=" + title. With the title A & B=C Mojarra split it into two parameters (the
page saw A), and MyFaces produced an invalid URL with a space and an ampersand. Encoding by hand did not help portably
either: URLEncoder.encode worked on Mojarra, but MyFaces encoded the already encoded value a second time. Prefer
<view-param> in a rule, or keep to identifiers (an ISBN or a numeric id) in the outcome string. Non-ASCII text in a
redirect parameter also depends on the character encoding: in an earlier run without any declared encoding, MyFaces encoded
ü in Latin-1 (%FC) and Tomcat answered the follow-up GET with status 500, while Mojarra used UTF-8. After declaring
the encoding in web.xml (Servlet 4.0 and later) both worked:
<request-character-encoding>UTF-8</request-character-encoding>
<response-character-encoding>UTF-8</response-character-encoding>
Source: Jakarta EE Tutorial: Configuring Navigation Rules, VDL: f:viewParam and Jakarta Servlet 6.1 specification
For the view parameters themselves (conversion, validation, 404 handling), the view parameters and view actions page is the reference.
Redirects After an Ajax Request
A command button with f:ajax can redirect too. The page below posts the same submit action as an Ajax request:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<h:head>
<title>Review a book (Ajax)</title>
</h:head>
<h:body>
<h:messages id="messages" globalOnly="true"/>
<h:form id="review">
<h:outputLabel for="title" value="Book"/>
<h:inputText id="title" value="#{reviewFormBean.bookTitle}" required="true"/>
<h:message id="titleMessage" for="title"/>
<h:commandButton id="send" value="Send" action="#{reviewFormBean.submit}">
<f:ajax execute="@form" render="@form messages"/>
</h:commandButton>
</h:form>
</h:body>
</html>
The response of an Ajax request is XML and not a page, so Faces answers with a redirect element and the Faces JavaScript
library navigates the browser (the Ajax chapter of the specification defines this element). Observed on both implementations:
<partial-response><redirect url="/bookshelf/review-done.xhtml"></redirect></partial-response>
The GET that follows found the review and the kept message, exactly as in the non-Ajax case. So PRG needs no special code for Ajax.
Flash System Events
The flash publishes system events, so you can audit or react to it: PostPutFlashValueEvent when a value is stored,
PostKeepFlashValueEvent when one is kept, PreRemoveFlashValueEvent when one is removed and PreClearFlashEvent
before the flash is cleared. The event source is the key (or, for the clear event, the map). The specification text that
lists the system events spells the first ones PostKeepFlashEvent and PostPutFlashEvent; the API classes carry Value in the
name, as written here.
Register a listener in faces-config.xml (see Core Tags for f:event on views and the
the events and listeners page for the model):
<application>
<system-event-listener>
<system-event-listener-class>org.example.bookshelf.web.FlashAudit</system-event-listener-class>
<system-event-class>jakarta.faces.event.PostPutFlashValueEvent</system-event-class>
</system-event-listener>
<system-event-listener>
<system-event-listener-class>org.example.bookshelf.web.FlashAudit</system-event-listener-class>
<system-event-class>jakarta.faces.event.PostKeepFlashValueEvent</system-event-class>
</system-event-listener>
</application>
package org.example.bookshelf.web;
import jakarta.faces.event.SystemEvent;
import jakarta.faces.event.SystemEventListener;
import java.util.logging.Logger;
public class FlashAudit implements SystemEventListener {
private static final Logger LOG = Logger.getLogger(FlashAudit.class.getName());
@Override
public void processEvent(SystemEvent event) {
LOG.info(() -> event.getClass().getSimpleName() + ": " + event.getSource());
}
@Override
public boolean isListenerForSource(Object source) {
return true;
}
}
With this registered, a PRG logged PostPutFlashValueEvent: lastReview on both implementations, and keeping the entry
logged PostKeepFlashValueEvent: lastReview. PreClearFlashEvent fired on both as well; PreRemoveFlashValueEvent was not triggered in these runs.
Source: API: PostPutFlashValueEvent and API: SystemEventListener
Mojarra and MyFaces
The observable behaviour was the same for the PRG, the kept messages, the single-use lifetime, flash.keep, Ajax redirects
and the events. The differences found:
| Mojarra 4.1.17 | MyFaces 4.1.4 | |
|---|---|---|
Cookie sent for the flash |
|
|
|
Flash survived |
Flash was lost |
Message queued in a request that has no session yet and ends in a redirect |
Worked |
|
Redirect outcome with hand-built query string |
Splits unencoded |
Unencoded value produced an invalid URL; pre-encoded values were encoded twice |
Where the entries are kept |
In an application-wide store found through the cookie; a second node with the replicated session did not see the entry |
In the HTTP session; a second node with the replicated session saw the entry (two-node run, see the clustering page) |
Context parameters (from the implementations' docs, not run) |
|
|
The cookie names are implementation details I observed, not something the specification defines; they may change between releases. The Mojarra documentation describes the first parameter as writing the flash cookie on every response and the second as the maximum number of concurrent flash users (default 5000, available since 4.0.7). The MyFaces documentation says the first one defines whether the flash scope is disabled (no flash cookie is added), and that the purge parameter lets server-side state saving delete old views even when PRG is used.
What Changed in Which Version
-
2.0: the flash scope,
ExternalContext.getFlash()and theflashEL object are introduced (Since: 2.0in the API documentation);setKeepMessagesandsetRedirectcome with it. -
2.2: the change log lists events from the flash (issue 766) and a
FlashFactory(issue 1071), the delegating implementation support behindFlashWrapper. The same change log lists a change of the default navigation algorithm to account forUIViewAction(the automaticsetKeepMessagesrule described above). -
4.x: the changes lists of recent 4.x releases mention an
HttpOnlyflash cookie (Mojarra and MyFaces both sent it in this run); the Flash documentation does not define a cookie. -
5.0 (upcoming): the milestone releases remove deprecated APIs; this page was not checked against them and nothing here should be read as describing 5.0.
Where the Books Are Outdated
The oldest books in the bibliography, JavaServer Faces in Action (2004), Mastering JavaServer Faces (2004) and Pro JSF
and Ajax (2006), were published before JSF 2.0 introduced the flash, so they cannot describe it. A JSF 1.x application
that wanted a Post/Redirect/Get had a <redirect/> navigation case, request parameters, or a session-scoped bean to hold the
confirmation data. The last is a poor trade: the data outlives its purpose and is shared by every tab of the session. The
flash replaces that bean with an entry that disappears by itself. JavaServer Faces 2.0: The Complete Reference and the
JSF 2.0 Cookbook target JSF 2.0, the release that added the flash, but their code uses the javax.faces packages and
@ManagedBean, which are now jakarta.faces and CDI. Everything on this page was checked on 4.1.
Pitfalls
-
Forgetting
setKeepMessages(true). The redirect works, the page loads, but the confirmation never appears. See Messages Across a Redirect. -
Storing big or non-serializable objects. On MyFaces the flash lives in the session, and a session that is replicated or persisted needs small
Serializablevalues; theReviewrecord implementsSerializablefor that reason. Do not count on the flash in a cluster, though: on Mojarra 4.1.17 the entry stayed on the node that stored it, and the redirect target found nothing when it ran on another node, even with a replicated session. Use sticky routing at least for the redirect, or carry the data in a query parameter (the clustering page has the two-node run). -
Reading the flash on a page that can be opened directly. After a reload or a bookmark the flash is empty. Guard such pages with an
f:viewActionas shown, or have them work without it. -
Two tabs and prefetching. The first request of the session after the redirect consumes the entry. See How Long a Flash Entry Lives.
-
ExternalContext.redirecton MyFaces. Callflash.setRedirect(true)first. See Three Ways to Redirect. -
MyFaces: a redirect with a message on the very first request of a session. A visitor whose first request is
review-done.xhtml(no session cookie yet) was redirected by the guard on both implementations, but on MyFaces 4.1.4 the response ended with500andjava.lang.IllegalStateException: Cannot create a session after the response has been committed, thrown fromFlashImpl._saveMessageswhile the flash tried to create the session. Mojarra handled the same request. The remedy used in the guard above is to create the session before queueing the message,facesContext.getExternalContext().getSession(true). With it, the first request got its302and a session cookie, and the message appeared on both implementations. An already existing session did not show the problem. -
Hand-built redirect URLs. See Flash or Query Parameters?.
-
Relying on the flash for forwards. A navigation without
faces-redirect=truekeeps the POST URL in the address bar and discards the flash entry after the response. -
Double clicks. PRG does not stop a user from clicking Send twice before the first response arrives. Make the operation idempotent or disable the button in the browser; PRG only solves the reload and back-button case (reasoning, not run).
References
-
Jakarta Faces 4.1 specification, 7.4.2 Default NavigationHandler Algorithm
-
Jakarta EE Tutorial: Using Ajax with Jakarta Faces Technology
-
Burns, Schalk: JavaServer Faces 2.0: The Complete Reference, McGraw-Hill, 2010, publisher page
-
Leonard: JSF 2.0 Cookbook, Packt, 2010, publisher page