Multi-Window Support
|
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), Eclipse Mojarra 4.1.17 and
Apache MyFaces Core 4.1.4, with Java 17 or later; the 4.0 behaviour is the same except where What Changed per Version says otherwise. Every
example was compiled and deployed in a scratch Bookshelf WAR on Apache Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and
once with MyFaces. The window ids, URLs, instance numbers and error messages quoted on this page come from scripted HTTP clients and
from headless Chrome with several tabs open (checked on 2026-10-08). Where the two implementations disagree, Mojarra and MyFaces Compared
collects the differences. Faces 5.0 is still a milestone release at the time of writing and nothing here describes it as current
behaviour.
A user who works with a web application opens a second tab. The two tabs share one cookie, and therefore one HTTP session, but each tab
shows its own page and expects its own state: a half-edited book in one, a search result in the other. Servlets have no notion of a
tab, so a Faces application that keeps page state in the session lets one tab silently overwrite the other. Faces 2.2 added the
ClientWindow feature to tell the tabs apart. This page explains the feature from the ground up: what a client window is, how to
switch it on, how its id reaches the server on every kind of request, the CDI scope built on it, how several tabs interact with the
server-side view state, how to extend it, and why its id is a label and not a credential.
The Problem: Several Tabs, One Session
Consider the Bookshelf librarian who edits two books at the same time, each in its own tab. If the edit form is backed by a session bean, both tabs read and write the same fields.
package org.example.bookshelf.web;
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Named;
import java.io.Serializable;
@Named
@SessionScoped
public class SessionDraft implements Serializable {
private String title = "";
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html" xmlns:f="jakarta.faces.core" xmlns:ui="jakarta.faces.facelets">
<h:head><title>Session edit</title></h:head>
<h:body>
<h:form id="f">
<h:inputText id="title" value="#{sessionDraft.title}"/>
<h:commandButton id="save" value="Save"/>
<h:outputText id="shown" value="title=#{sessionDraft.title}"/>
</h:form>
</h:body>
</html>
In the test one browser profile (one cookie jar) opened session-edit.xhtml twice, as two tabs would. The first tab saved the text
tab-1 text. The second tab, opened afterwards, rendered title=tab-1 text: the draft of the first tab appeared in the second, on both
implementations. Had the second tab saved its own text, the first tab would have shown it after its next postback. This is the
cross-tab bug that the scopes page of this section warns about (see Scopes); the cure is to
choose a scope whose instances are not shared by all tabs.
| Scope | Separates tabs | Survives navigation to another page in the tab | Notes |
|---|---|---|---|
|
no |
yes |
One instance per session: every tab sees the same fields. |
|
yes |
no |
One instance per view; navigation to another view ends it (see When the View Scope Ends). |
|
yes, if the |
yes, until the conversation ends |
Weld propagates |
|
yes |
yes, inside the flow |
Flow state is kept per client window (see Faces Flows). |
|
yes |
yes |
One instance per client window. Needs the client window mode switched on. |
@ClientWindowScoped is the scope for state that belongs to a tab but outlives a single view, such as a draft that the user carries
from the catalog to the edit page and back. It is built on the ClientWindow class, which the rest of this page explains.
What a Client Window Is
The API documentation of jakarta.faces.lifecycle.ClientWindow describes it as the thing that may be "a browser tab, browser window,
browser pop-up, portlet, or anything else that can display a UIComponent hierarchy rooted at a `UIViewRoot`". Its lifetime starts with
the first request that window makes to the Faces runtime and lasts as long as the window remains open or the session expires,
whichever comes first. A client window is associated with exactly one view root at a time but displays many different ones during its
life. The server cannot see a tab being closed, so in practice the lifetime ends when the session ends or when the window is evicted
(Per-Tab State with @ClientWindowScoped).
ClientWindow is an abstract class. A standard implementation is supplied by the runtime; the application sees it through these
members:
| Member | Purpose |
|---|---|
|
A string that identifies the window within the current session. |
|
Examines the incoming request and extracts the value that |
|
Called whenever the runtime generates a URL that needs window-related parameters; the returned map must be immutable. A custom implementation can add its own parameters. See Custom Client Windows: ClientWindowFactory and ClientWindowWrapper for what the two implementations do with it. |
|
Asked by every method that appends the window to a generated URL. In |
|
A component that offers per-use opt-out calls |
|
The constants for the context parameters |
Around the class there are five more pieces:
-
ExternalContext.getClientWindow()andsetClientWindow(ClientWindow)hold the instance that belongs to the current request. The getter returnsnullwhen the feature is off. -
Lifecycle.attachWindow(FacesContext)creates the instance with theClientWindowFactory, calls itsdecodeand stores it on theExternalContext. -
ClientWindowFactory(with the decoratorClientWindowWrapper) creates theClientWindowfor a request. It returnsnullwhen the mode isnoneor unspecified. -
ResponseStateManager.CLIENT_WINDOW_PARAMandCLIENT_WINDOW_URL_PARAMname the hidden field and the URL parameter. In the test the values werejakarta.faces.ClientWindowandjfwid. -
@ClientWindowScoped, the CDI scope, is described in Per-Tab State with @ClientWindowScoped.
Where the Client Window Sits in the Lifecycle
The client window is attached before the lifecycle starts. The FacesServlet sources (the API documentation of FacesServlet.service gives
the same order) ask the resource handler whether the request is a resource request. If it is, the resource is served and there is no client
window. If it is not, the servlet calls attachWindow, then execute (phases 1 to 5) and then render (see
The Phases). Consequently:
-
A
PhaseListeneralready sees the client window inbeforePhaseof Restore View, and the window is the same in all six phases of the request. The test listener below logged the same id frombefore RESTORE_VIEWtoafter RENDER_RESPONSEon both implementations. -
Requests for
jakarta.faces.resourceURLs (stylesheets, scripts,faces.js) have no client window. -
Ajax requests and postbacks attach the window exactly as initial requests do; the id comes from the hidden field of the submitted form.
package org.example.bookshelf.web;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.faces.event.PhaseEvent;
import jakarta.faces.event.PhaseId;
import jakarta.faces.event.PhaseListener;
import jakarta.faces.lifecycle.ClientWindow;
public class WindowTracePhaseListener implements PhaseListener {
@Override
public PhaseId getPhaseId() {
return PhaseId.ANY_PHASE;
}
@Override
public void beforePhase(PhaseEvent event) {
trace("before", event);
}
@Override
public void afterPhase(PhaseEvent event) {
trace("after ", event);
}
private void trace(String when, PhaseEvent event) {
ClientWindow window = event.getFacesContext().getExternalContext().getClientWindow();
CDI.current().select(EventLog.class).get().add(
when + " " + event.getPhaseId().getName() + " window=" + (window == null ? "null" : window.getId()));
}
}
<lifecycle>
<phase-listener>org.example.bookshelf.web.WindowTracePhaseListener</phase-listener>
</lifecycle>
The diagram shows two tabs of one browser against one server. Every request carries the id of the tab that sent it, which is how the
server finds the right @ClientWindowScoped instance.
Switching It On: jakarta.faces.CLIENT_WINDOW_MODE
The feature is off by default. The context parameter jakarta.faces.CLIENT_WINDOW_MODE switches it on:
<context-param>
<param-name>jakarta.faces.CLIENT_WINDOW_MODE</param-name>
<param-value>url</param-value>
</context-param>
<context-param>
<param-name>jakarta.faces.NUMBER_OF_CLIENT_WINDOWS</param-name>
<param-value>10</param-value>
</context-param>
Source: Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters and Jakarta Faces 4.1 API: ClientWindow
The specification requires every implementation to support none and url; any value the implementation does not understand means
none. jakarta.faces.NUMBER_OF_CLIENT_WINDOWS ("Since 4.0", default 10 in both implementations) bounds how many windows per session
@ClientWindowScoped keeps and is only active when the mode is enabled.
| Value | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
unset |
Feature off, unless the application defines a Faces flow, which switches |
Feature off, unless the application defines a flow, which switches |
|
Feature off. |
Same. |
|
Window id in the |
Same parameters. Implementation class |
|
Not supported: treated as |
Documented by MyFaces as a value of the parameter. In the 4.1.4 test every plain |
|
Not supported: treated as |
Window id kept in the browser’s |
Without the mode, the feature is simply not there. ExternalContext.getClientWindow() returns null, no hidden field is rendered, no
parameter is added to URLs, and the first request that touches a @ClientWindowScoped bean fails with ContextNotActiveException:
WELD-001303: No active contexts for scope type jakarta.faces.lifecycle.ClientWindowScoped. That was the result for none, for the
parameter missing and for an unknown value (bogus), on both implementations. With the mode url and no flow involved the page worked
unchanged on both.
The MyFaces-Only Modes
MyFaces documents four values for the parameter: none, url, url-redirect and client.
url-redirect. The source of UrlRedirectClientWindow shows the intent: a GET without a window id is answered by a redirect to the same
URL with a freshly generated id, so that the address bar always contains the id. In the 4.1.4 test the redirect never carried the id.
curl -L followed five redirects to the same address /bookshelf/catalog.xhtml and gave up, on a first request without cookies. The
reason, visible in the source, is the order of events: the redirect URL is encoded in decode, before the new ClientWindow has been
stored on the ExternalContext, and the URL encoder appends jfwid only for a stored window. Requests that already carried
jfwid (for example ?jfwid=abc) were served normally. Treat the mode as unusable in 4.1.4 until a release note says otherwise.
client. The window id lives in the browser’s window.name instead of the URL. The first GET of a tab is answered by a small HTML
page (org.apache.myfaces.windowId/windowhandler.html) whose script stores the id in window.name, hands it back to the server in a cookie named
jfwid-<number>, and reloads the original URL with jfwid and a request token mfRid appended. The page has a
noscript link for browsers without JavaScript, and in that case the id cannot be kept. In headless Chrome the mode behaved like this:
-
the first tab got one window id, a second tab got a different one;
-
a hand-typed URL without any id, loaded in the same tab, kept the window of that tab, because
window.namesurvives navigation. Inurlmode the same action starts a new window; -
the URLs in the address bar are cluttered with
jfwidandmfRid, and after anh:linkclick the address contained the page URL a second time as the first query parameter, which was harmless but ugly.
The mode therefore makes plain <a href> links and bookmarks tab-aware, at the cost of an extra round trip for every initial GET, JavaScript, and
a first response that is not the requested page. A crawler or a curl script sees only the loader page.
How the Window Id Travels
In url mode the runtime puts the window id into every request that originates from a Faces-generated page. The test page below
contains each kind of link on purpose.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html" xmlns:f="jakarta.faces.core" xmlns:ui="jakarta.faces.facelets">
<h:head><title>Catalog</title></h:head>
<h:body>
<h1>Catalog</h1>
<h:outputText id="win" value="window=#{windowInfo.id}"/>
<h:outputText id="editor" value=" editor=#{bookEditor.instance} title=#{bookEditor.title}"/>
<h:form id="f">
<ui:repeat value="#{bookCatalog.books}" var="b">
<h:commandLink id="edit" value="Edit #{b.title}" action="#{bookEditor.edit(b.isbn)}"/>
</ui:repeat>
</h:form>
<h:link id="hl" outcome="edit" value="Edit (h:link)"/>
<h:button id="hb" outcome="edit" value="Edit (h:button)"/>
<h:link id="nolink" outcome="edit" value="Edit (no window id)" disableClientWindow="true"/>
<h:outputLink id="ol" value="edit.xhtml">Edit (h:outputLink)</h:outputLink>
<a id="plain" href="edit.xhtml">Edit (plain a)</a>
</h:body>
</html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html" xmlns:f="jakarta.faces.core" xmlns:ui="jakarta.faces.facelets">
<h:head><title>Edit</title></h:head>
<h:body>
<h1>Edit</h1>
<h:outputText id="win" value="window=#{windowInfo.id}"/>
<h:outputText id="editor" value=" editor=#{bookEditor.instance}"/>
<h:form id="f">
<h:inputText id="title" value="#{bookEditor.title}"/>
<h:commandButton id="save" value="Save"/>
<h:commandButton id="again" value="Again" action="edit"/>
<h:commandButton id="redir" value="Redirect" action="edit?faces-redirect=true"/>
<h:commandButton id="ajax" value="Ajax"><f:ajax execute="title" render="shown"/></h:commandButton>
<h:outputText id="shown" value="title=#{bookEditor.title}"/>
</h:form>
<h:link id="back" outcome="catalog" value="Catalog"/>
</h:body>
</html>
Source: Jakarta Faces 4.1 VDL: h:link, Jakarta Faces 4.1 VDL: h:button and Jakarta Faces 4.1 API: ExternalContext
The HTML that Mojarra generated for catalog.xhtml (ids shortened to W1; the jsessionid path parameter, which Tomcat adds to the first response for a client that has no session cookie yet, is left out):
<form id="f" name="f" method="post" action="/bookshelf/catalog.xhtml?jfwid=W1" enctype="application/x-www-form-urlencoded">
...
<input type="hidden" name="jakarta.faces.ClientWindow" id="j_id1:jakarta.faces.ClientWindow:0" value="W1" autocomplete="one-time-code" />
</form>
<a id="hl" name="hl" href="/bookshelf/edit.xhtml?jfwid=W1">Edit (h:link)</a>
<input id="hb" type="button" onclick="window.location.href='/bookshelf/edit.xhtml?jfwid=W1'; return false;" value="Edit (h:button)">
<a id="nolink" name="nolink" href="/bookshelf/edit.xhtml">Edit (no window id)</a>
<a id="ol" name="ol" href="edit.xhtml">Edit (h:outputLink)</a>
<a id="plain" href="edit.xhtml">Edit (plain a)</a>
| Source of the request | Id sent | How |
|---|---|---|
Postback of an |
yes |
In the query string of the form |
|
yes |
|
Navigation outcome with |
yes |
The |
Ajax request ( |
yes |
Part of the submitted form: the hidden field is posted with it. |
Ajax response |
yes |
The partial response contains an |
|
no |
It renders its |
Hand-written |
no |
The request starts a new window, exactly as a new tab does. |
Resource URLs ( |
no |
No client window is attached to resource requests. |
Source: Jakarta Faces 4.1 API: ResponseStateManager and Jakarta Faces 4.1 specification, 8 Rendering Model
Three consequences follow, and the third surprises most users:
-
A link that must stay in the same tab has to be an outcome-target or command component. Use
h:link,h:button,h:commandLinkor a navigation rule. A literal<a href>orh:outputLinksilently starts a new window, and a@ClientWindowScopedbean then starts over. -
Opting out is rare but legitimate. A link that opens an unrelated page in a new tab should say
disableClientWindow="true": then the new tab does not share the state of the tab that opened it. In the test the link rendered withoutjfwidon both implementations. The 4.1 VDL documents the attribute onh:linkandh:buttononly. -
The id is in the address bar only after the first Faces link or redirect. The first
GETof a tab has no id in its URL; the server fabricates one and writes it into the page. After the user clicks anh:link, or after a redirect, the address contains thejfwidparameter. Copying that URL into another tab (or using the browser’s "duplicate tab" command on such a page) therefore points the new tab at the same window. In headless Chrome the copy showed theEditform of the first tab with the first tab’s draft text, and the sameBookEditorinstance, on both implementations. A tab duplicated before any Faces link was followed has no id in its URL and becomes a new window.
Reading the Window Id
The id is useful beyond scoping: to tag log lines, to correlate Ajax requests, or to key a client-side cache. Faces offers three ways to read it.
Java. FacesContext.getCurrentInstance().getExternalContext().getClientWindow() returns the instance or null:
package org.example.bookshelf.web;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.context.FacesContext;
import jakarta.faces.lifecycle.ClientWindow;
import jakarta.inject.Named;
@Named
@RequestScoped
public class WindowInfo {
public String getId() {
ClientWindow window = FacesContext.getCurrentInstance().getExternalContext().getClientWindow();
return window == null ? "no client window" : window.getId();
}
public String getType() {
ClientWindow window = FacesContext.getCurrentInstance().getExternalContext().getClientWindow();
return window == null ? "null" : window.getClass().getName();
}
}
EL. #{facesContext.externalContext.clientWindow.id} works because facesContext is an implicit object (see
Implicit Objects); it rendered the same id as the Java accessor on both
implementations, and an empty string when the feature is off.
JavaScript. faces.getClientWindow(node) returns the window id of the form that contains the node, searching document.forms when called with no
argument. It throws if the page holds more than one distinct id and returns null when none can be found. It needs faces.js on the page.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<h:head>
<title>Window id in JavaScript</title>
<h:outputScript library="jakarta.faces" name="faces.js" target="head"/>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('js-window').textContent = faces.getClientWindow() || 'none';
});
</script>
</h:head>
<h:body>
<h:form id="f">
<h:commandButton id="go" value="Reload part">
<f:ajax render="@form"/>
</h:commandButton>
</h:form>
<p>This tab is window <code id="js-window"></code></p>
</h:body>
</html>
In headless Chrome the paragraph showed the same id as the hidden field before and after an Ajax request, on both implementations. A script that sends data to a server endpoint outside Faces can include the id as a header or parameter, for example to let a WebSocket or SSE endpoint address the right tab. Keep in mind what The Window Id Is Not a Credential says about trusting it.
Per-Tab State with @ClientWindowScoped
@ClientWindowScoped (jakarta.faces.lifecycle, "Since 4.0") is a normal, passivating CDI scope. The runtime supplies an extension that
creates the bean lazily, when it is referenced and accessed. One instance exists per client window, and a window is identified by its id.
Because the scope is passivating, the bean class must be Serializable and everything injected into it must be passivation capable
(the scopes page has the rules, see Scopes: @ClientWindowScoped and
Serialisation, Passivation and Memory). A transient injected field is fine, as in the example.
The Bookshelf editor keeps the draft title that one tab is working on:
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.faces.lifecycle.ClientWindowScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.concurrent.atomic.AtomicInteger;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
/** The draft of the book that one browser window is editing. */
@Named
@ClientWindowScoped
public class BookEditor implements Serializable {
private static final AtomicInteger SEQUENCE = new AtomicInteger();
private final int instance = SEQUENCE.incrementAndGet();
@Inject
private transient BookService books;
@Inject
private EventLog log;
private String isbn;
private String title = "";
@PostConstruct
void created() {
log.add("created BookEditor #" + instance);
}
@PreDestroy
void destroyed() {
log.add("destroyed BookEditor #" + instance);
}
public String edit(String isbn) {
this.isbn = isbn;
this.title = books.find(isbn).map(Book::title).orElse("");
return "edit";
}
public int getInstance() {
return instance;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
The pages catalog.xhtml and edit.xhtml above use it. A scripted session with one cookie jar showed the following on both
implementations:
| Step | Window | BookEditor |
|---|---|---|
Open |
W1 |
new instance, no title |
Click the |
W1 |
same instance, title |
Follow |
W1 |
same instance, the catalog shows |
Open |
W2 |
another instance, no title |
Follow the plain |
W3 |
a third instance, no title |
Send a |
W1 again |
a new instance (the session and its windows were destroyed), no title |
The draft travelled with the tab through three requests and four URL forms, and a second tab never saw it. This is the behaviour that the session bean of The Problem: Several Tabs, One Session cannot provide.
The lifetime has three endpoints:
-
Session end. When the session is invalidated or times out, its windows end with it. A request with an old window id in a new session gets a new instance. MyFaces called the
@PreDestroymethod ofBookEditoron invalidation. Mojarra did not: the log never showeddestroyed BookEditorfor a client window bean, in line with the source of itsClientWindowScopeContextManager, which only clears the bean maps when the session is destroyed. Do not rely on@PreDestroyof a@ClientWindowScopedbean to release resources on Mojarra. -
Eviction. The window count per session is bounded by
jakarta.faces.NUMBER_OF_CLIENT_WINDOWS(default 10). When an eleventh window appears, the least recently used one is dropped and its next request gets a fresh bean (see Scopes for the measured behaviour with the limit set to 2). A user who keeps many tabs open on a long-lived session can therefore lose a draft; raise the limit if that matters and mind the memory, since each window keeps its beans. -
The tab is closed. Nothing happens at that moment: the server does not know.
Anything that must outlive the session (a draft the user should find tomorrow) belongs in a database, not in this scope.
Several Tabs and the View State
The client window and the view state are two separate mechanisms. The client window id selects the @ClientWindowScoped instances. The view state is
what Faces saves so that it can rebuild the component tree on a postback. With the default server-side state saving
(jakarta.faces.STATE_SAVING_METHOD = server) the state is stored in the HTTP session and the page carries only a token, the hidden field
jakarta.faces.ViewState. The session is shared by all tabs, and so are the limits that bound how many view states it keeps.
| Mojarra 4.1.17 | MyFaces 4.1.4 | Meaning |
|---|---|---|
|
|
How many views per session keep their saved state. The oldest is dropped when the limit is exceeded. |
A logical view in Mojarra is an initially requested view plus all of its postbacks. Every plain GET creates one. A tab that follows an
h:link creates one, and so does a new tab, so a user with many tabs open uses up the budget of the session quickly. The test shows what
happens when one tab is busy while the other waits:
| Step | Mojarra (numberOfLogicalViews = 2) |
MyFaces (NUMBER_OF_VIEWS_IN_SESSION = 2) |
|---|---|---|
Tab A loads |
200, |
200, |
Tab B loads |
200 each |
200 each |
Tab A saves again |
500 |
500 |
Tab A reloads its address |
200, |
200, |
Three conclusions follow.
-
The limits are per session, not per window. Whatever one tab does counts against the other. With the defaults (15 and 20) the effect appears only when someone has many tabs open on pages with forms, but it is real, and it shows up as a
ViewExpiredExceptionon the postback of a tab that the user left alone for a while. -
The per-window state is not lost with the view state. The
BookEditorkept the draft after the view had expired. A page that reloads byGETon its own window id recovers. This is why@ClientWindowScoped(or a database) is the right place for a draft, and a view scope bean is not: the view scope instance disappears with its view state. -
Client-side state saving removes the limit. With
jakarta.faces.STATE_SAVING_METHOD=clientthe state travels in the page, nothing is counted in the session, and Tab A posted successfully after Tab B had loaded three pages, on Mojarra. The price is page size and the CPU cost of encoding the state (Mojarra encrypts it unlesscom.sun.faces.disableClientStateEncryptionis set, a switch meant for debugging). See the page on state saving and stateless views.
For the user’s sake handle the exception instead of showing an error page: reload the view, or redirect to the same view id, so that the tab
continues with its window state. The Ajax page of this section shows a client-side addOnError handler that does it for Ajax requests, see
Ajax: Errors; the page on exception handling covers the server side. A view that needs no state at all can be
a stateless view (transient="true" on f:view), which has no per-session cost and no expiry. Sizing the view-state limits for many tabs is
treated on the performance page.
A last relation between tabs and views is the view scope. Each tab has its own view, so two tabs showing the same page get two instances of a
@ViewScoped bean (in the test, view=1 and view=2 in two tabs of vs.xhtml). Navigation inside a tab ends the view scope instance of that tab only.
The flash scope is a different case, described in Flash and Post/Redirect/Get.
Custom Client Windows: ClientWindowFactory and ClientWindowWrapper
The runtime creates a ClientWindow through a ClientWindowFactory, and the factory is one of the artefacts that an application may
replace or decorate in faces-config.xml (see the decorator pattern in
11.3.7 Delegating Implementation Support). The decorated factory
receives the previous factory in its constructor, and ClientWindowWrapper is the matching wrapper for the window itself. Typical reasons to
write one:
-
Validate the id. The standard windows accept whatever string the client sends (The Window Id Is Not a Credential). A wrapper can insist on a format.
-
Add parameters to generated URLs through
getQueryURLParameters, for example a tenant or an area of the application. -
Another transport for non-browser clients, or a different place for the id, by extending
ClientWindowdirectly. Itsdecodemust makegetId()return a value that is suitable for a hidden field and a query parameter.
The Bookshelf example validates the id. A well-formed id (letters, digits, colon, underscore and hyphen, at most 64 characters) is kept; anything else is replaced by a freshly generated id, which starts a new window.
package org.example.bookshelf.window;
import jakarta.faces.context.FacesContext;
import jakarta.faces.lifecycle.ClientWindow;
import jakarta.faces.lifecycle.ClientWindowWrapper;
import java.util.UUID;
import java.util.regex.Pattern;
/** Accepts only well-formed window ids; anything else is replaced by a fresh one. */
public class SafeClientWindow extends ClientWindowWrapper {
private static final Pattern VALID = Pattern.compile("[A-Za-z0-9:_-]{1,64}");
private String id;
public SafeClientWindow(ClientWindow wrapped) {
super(wrapped);
}
@Override
public void decode(FacesContext context) {
super.decode(context);
String candidate = super.getId();
id = candidate != null && VALID.matcher(candidate).matches() ? candidate : UUID.randomUUID().toString();
}
@Override
public String getId() {
return id;
}
}
package org.example.bookshelf.window;
import jakarta.faces.context.FacesContext;
import jakarta.faces.lifecycle.ClientWindow;
import jakarta.faces.lifecycle.ClientWindowFactory;
public class SafeClientWindowFactory extends ClientWindowFactory {
public SafeClientWindowFactory(ClientWindowFactory wrapped) {
super(wrapped);
}
@Override
public ClientWindow getClientWindow(FacesContext context) {
ClientWindow window = getWrapped().getClientWindow(context);
return window == null ? null : new SafeClientWindow(window);
}
}
<factory>
<client-window-factory>org.example.bookshelf.window.SafeClientWindowFactory</client-window-factory>
</factory>
With the factory registered, a GET edit.xhtml?jfwid=<value> produced the following on both implementations; the hidden field, the
visible window id and the form action all showed the id that the wrapper returned:
jfwid sent by the client |
Window used |
|---|---|
|
|
|
|
|
a generated UUID |
an empty value |
a generated UUID |
|
a generated UUID |
Notes from the run:
-
A decorator that overrides
getId()must keep its own copy of the validated value, as the example does: callingsuper.getId()again would return the unvalidated id of the wrapped window. -
Do not return
jfwidfromgetQueryURLParameters. Both runtimes append the standard parameter themselves. Returning it as well produced a duplicated parameter in the Mojarra form action (?jfwid=W1=jfwid=W1). -
Extra parameters work in links but not in Mojarra forms. With
getQueryURLParametersreturningMap.of("area", "books"), MyFaces produced?area=books&jfwid=W1in the form action and in links. Mojarra 4.1.17 produced?jfwid=W1&area=booksinh:linkURLs but the malformed?jfwid=W1=area=booksin the form action, so the extra parameter was not delivered as a parameter by postbacks. Until that is fixed, carry extra window data in the id itself or in a@ClientWindowScopedbean instead of in additional URL parameters. -
A decorated factory must still return
nullwhen the feature is off, because it receives thatnullfrom the wrapped factory. The example does.
The Window Id Is Not a Credential
The window id separates the state of tabs, nothing else. The run showed how little the runtime checks it, on both implementations:
-
A request with
jfwid=my-own-idcreated a window with exactly that id and its ownBookEditor. A second request with the same value received the same instance. The runtime does not require the id to be one it issued. -
An empty value, a 300-character value and a value with spaces were all accepted as ids. A value containing
"><script>alert(1)</script>was written into the hidden field, the visible text and the form action HTML-escaped ("><script>and so on); no raw<script>reached the page. Mojarra left spaces unencoded in the form action URL, MyFaces encoded them as+. -
The ids differ in how guessable they are. Mojarra builds a random UUID followed by a per-session counter. The MyFaces ids from one session in the test differed only in their last character (
wve5kxm2i,wve5kxm2j,wve5kxm2k). -
The API documentation says that a found value is decrypted with a key from the session. Neither implementation rejected a client-chosen value in the test.
What follows from that:
-
Never use the window id for authorization. A user who changes
jfwidgets a different set of window beans in their own session, which is harmless. An application that used the id to mean "this tab belongs to that case file" would let the user claim any case file. -
Do not use it as the only key of anything shared between sessions. A cache or a table keyed by window id alone mixes the data of two users who happen to send the same string, and with sequential ids a user can guess a neighbour’s. Combine it with the session or user identity.
-
Treat it as untrusted input whenever it leaves the page: when you write it to a log, put it in a header or pass it to a script. The
SafeClientWindowof Custom Client Windows: ClientWindowFactory and ClientWindowWrapper restricts it to a safe alphabet at the point of entry. -
The session cookie stays the credential. State of other users is out of reach, because windows live in the session of the user who owns them. Keep the cookie
Secure,HttpOnlyandSameSite, and do not create sessions for anonymous requests that do not need one. -
Bound the damage.
jakarta.faces.NUMBER_OF_CLIENT_WINDOWScaps the number of windows per session; a script that sends many distinct ids cycles through that cache of its own session instead of growing it. -
Links to other sites carry no id.
h:outputLinkand literal links are not decorated. If you build an external URL that includes the id (for example for a support portal), the id ends up in the other site’s logs andRefererheaders.
Testing with Several Windows
A test that wants two tabs must behave like two tabs: same cookie, different window ids. The script below plays two tabs with curl
(the jar is the browser profile). Each request without jfwid is a new tab; a request with the id of tab A belongs to tab A.
#!/bin/bash
# Two "tabs" of one browser: same cookie jar, two windows
BASE=http://localhost:8460/bookshelf
JAR=$(mktemp)
window_id() { # print the window id found in the hidden field of a page
grep -o 'name="jakarta.faces.ClientWindow"[^>]*value="[^"]*"\|value="[^"]*"[^>]*name="jakarta.faces.ClientWindow"' \
| sed 's/.*value="\([^"]*\)".*/\1/' | head -1
}
TAB_A=$(curl -s -c "$JAR" -b "$JAR" "$BASE/edit.xhtml" | window_id)
TAB_B=$(curl -s -c "$JAR" -b "$JAR" "$BASE/edit.xhtml" | window_id)
echo "tab A: $TAB_A"
echo "tab B: $TAB_B"
# A request that carries the id of tab A belongs to tab A, even though the cookie is the same
curl -s -b "$JAR" "$BASE/edit.xhtml?jfwid=$TAB_A" | grep -o 'window=[^<]*'
rm -f "$JAR"
Output on both implementations (ids differ per run):
tab A: 8837fd44-9c83-4600-9f3d-3de7f77d88be:0
tab B: 00147af0-3122-46d7-a761-0dc874283f8e:1
window=8837fd44-9c83-4600-9f3d-3de7f77d88be:0
(On MyFaces the ids were short strings such as niramtxx6 and niramtxx7.)
For browser tests (Selenium, Playwright) the rule is the same. Every driver.get(url) to a Faces page is a request without an id, so a test that
navigates by URL starts a new window on every step and loses @ClientWindowScoped state. Navigate by clicking Faces links and buttons, or
read the id with faces.getClientWindow() and append ?jfwid= to the URLs you open. A second browser tab or Playwright page in the same
browser context is a second window with the shared session cookie, which is the scenario worth testing for any state that belongs to a tab. The testing page covers
browser tests in general.
Mojarra and MyFaces Compared
| Aspect | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
Window id shape |
Random UUID plus a per-session counter, for example a UUID followed by |
Short token, consecutive within a session (for example |
Supported modes |
|
|
|
treated as |
redirects forever in the test (the id is missing from the redirect URL) |
|
treated as |
window id in |
Flows with the parameter unset |
switches |
switches |
Precedence of hidden field and URL parameter |
URL parameter read only when the render mode is enabled; the hidden field always wins |
hidden field first, then the URL parameter |
|
never called (only cleared) |
called when the session ends |
Extra parameters from |
correct in links, malformed in the form |
correct everywhere |
Encoding of the id in URLs |
|
percent-encoded everywhere ( |
Window id in the Ajax response |
|
same |
What Changed per Version
| Version | Change |
|---|---|
2.2 |
The |
2.3 |
|
3.0 |
The rename to |
4.0 |
|
4.1 |
No change in the |
5.0 (upcoming) |
Faces 5.0 is a milestone release; this page does not describe it. |
Where the Books Are Outdated
-
The JSF 1.x books do not know that tabs exist. The books written for JSF 1.x (Mann, Jacobi and Fallows and their contemporaries) treat the session as the place for anything that survives a request and say nothing about two windows of one user. The pattern they teach, state in a session bean, is the cross-tab bug of The Problem: Several Tabs, One Session. The ClientWindow feature arrived in 2.2, years after those books.
-
The JSF 2.0 and 2.1 books know the view scope and the Seam 2 conversation. Neither tells tabs apart without extra work. Hand-made tokens in hidden fields and
window.namescripts were common workarounds in applications of that era, and Seam 2 has ended. Today the same job is the standardClientWindow, and thewindow.nameidea survives only in the MyFacesclientmode. -
javaxnames. Text and sample code for 2.2 and 2.3 usejavax.faces.lifecycle.ClientWindowandjavax.faces.CLIENT_WINDOW_MODE. Faces 4.x needsjakarta.faces.*(What Changed per Version). -
None of the books covers
ClientWindowor@ClientWindowScoped. The Jakarta EE Tutorial chapters on Faces configuration and the lifecycle do not mention the client window either, so the API documentation and the MyFaces configuration tables are the sources. -
Session-scoped "wizard" beans. Older examples keep a wizard’s data in the session. It breaks with two tabs; use a flow (Faces Flows), a conversation or
@ClientWindowScopedbean.
Common Mistakes
-
Using a
@SessionScopedbean for page state. Two tabs overwrite each other (The Problem: Several Tabs, One Session). Use@ViewScoped,@ClientWindowScopedor a flow. -
Using
@ClientWindowScopedwithout the mode.WELD-001303on the first request. Setjakarta.faces.CLIENT_WINDOW_MODEtourl. -
Linking with a literal
<a href>orh:outputLink. The link starts a new window and the bean starts over. Useh:link,h:button,h:commandLinkor a navigation rule. -
Testing with
driver.get()for every page. Each call is a new window. Navigate with clicks or passjfwid. -
Trusting the window id. It is a label chosen by the client (The Window Id Is Not a Credential).
-
Duplicating a tab and expecting independent state. A tab duplicated after a Faces link shares the first tab’s window.
-
Relying on
@PreDestroyof@ClientWindowScopedbeans on Mojarra. It is not called; MyFaces calls it. -
A non-serializable
@ClientWindowScopedbean. Deployment fails withWELD-000072; the scopes page lists the rules. -
Ignoring the view-state limits. An idle tab expires when other tabs of the same session create many views (
ViewExpiredException). The@ClientWindowScopedstate survives; reload the tab. -
Returning
jfwidfromgetQueryURLParametersor relying on extra parameters in Mojarra forms. See Custom Client Windows: ClientWindowFactory and ClientWindowWrapper. -
Using the MyFaces
url-redirectmode in 4.1.4. It redirected forever in the test. -
Keeping a draft in
@ClientWindowScopedbeans for longer than the session. The state ends with the session. Persist what must survive.
References
-
Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters
-
Jakarta Faces 4.1 specification, 11.3.7 Delegating Implementation Support
-
Jakarta Faces 4.1 specification, B.1 Changes between 4.1 and 4.0
-
Jakarta Faces 4.1 specification, B.2 Changes between 4.0 and 3.0
-
Apache MyFaces Core 4.1: configuration tables and requirements