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.

SessionDraft.java and session-edit.xhtml — page state in the session, shown only to demonstrate the problem
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.

Table 1. Which scope separates tabs
Scope Separates tabs Survives navigation to another page in the tab Notes

@SessionScoped

no

yes

One instance per session: every tab sees the same fields.

@ViewScoped

yes

no

One instance per view; navigation to another view ends it (see When the View Scope Ends).

@ConversationScoped

yes, if the cid travels with every request

yes, until the conversation ends

Weld propagates cid with form posts and redirects, not with h:link (see Scopes).

@FlowScoped

yes

yes, inside the flow

Flow state is kept per client window (see Faces Flows).

@ClientWindowScoped

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:

Table 2. The members of ClientWindow
Member Purpose

getId()

A string that identifies the window within the current session.

decode(FacesContext)

Examines the incoming request and extracts the value that getId() returns. The standard url mode looks for the hidden field jakarta.faces.ClientWindow and then for the URL parameter jfwid, and fabricates a new id if neither exists. It must not be called when the mode is none.

getQueryURLParameters(FacesContext)

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.

isClientWindowRenderModeEnabled(FacesContext)

Asked by every method that appends the window to a generated URL. In url mode all GET URLs carry the window by default.

disableClientWindowRenderMode(FacesContext) and enableClientWindowRenderMode(FacesContext)

A component that offers per-use opt-out calls disableClientWindowRenderMode before rendering a URL and enableClientWindowRenderMode in a finally block afterwards. h:link and h:button use them for their disableClientWindow attribute (How the Window Id Travels).

CLIENT_WINDOW_MODE_PARAM_NAME, NUMBER_OF_CLIENT_WINDOWS_PARAM_NAME

The constants for the context parameters jakarta.faces.CLIENT_WINDOW_MODE and jakarta.faces.NUMBER_OF_CLIENT_WINDOWS.

Around the class there are five more pieces:

  • ExternalContext.getClientWindow() and setClientWindow(ClientWindow) hold the instance that belongs to the current request. The getter returns null when the feature is off.

  • Lifecycle.attachWindow(FacesContext) creates the instance with the ClientWindowFactory, calls its decode and stores it on the ExternalContext.

  • ClientWindowFactory (with the decorator ClientWindowWrapper) creates the ClientWindow for a request. It returns null when the mode is none or unspecified.

  • ResponseStateManager.CLIENT_WINDOW_PARAM and CLIENT_WINDOW_URL_PARAM name the hidden field and the URL parameter. In the test the values were jakarta.faces.ClientWindow and jfwid.

  • @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 PhaseListener already sees the client window in beforePhase of Restore View, and the window is the same in all six phases of the request. The test listener below logged the same id from before RESTORE_VIEW to after RENDER_RESPONSE on both implementations.

  • Requests for jakarta.faces.resource URLs (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.

WindowTracePhaseListener.java — logs the window id in every phase
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()));
    }
}
WEB-INF/faces-config.xml (fragment) — registering the listener
<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.

sequenceDiagram participant A as Tab A participant B as Tab B participant S as Faces server (one session) A->>S: GET /catalog.xhtml (no window id) Note over S: attachWindow creates window W1 S-->>A: page with W1 in links, form action and hidden field B->>S: GET /catalog.xhtml (no window id) Note over S: attachWindow creates window W2 S-->>B: page with W2 in links, form action and hidden field A->>S: POST /catalog.xhtml?jfwid=W1 (hidden field W1) Note over S: BookEditor of W1 is reused S-->>A: page for W1 B->>S: POST /catalog.xhtml?jfwid=W2 (hidden field W2) Note over S: BookEditor of W2 is reused S-->>B: page for W2

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:

WEB-INF/web.xml (fragment)
<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>

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.

Table 3. The values of CLIENT_WINDOW_MODE
Value Mojarra 4.1.17 MyFaces 4.1.4

unset

Feature off, unless the application defines a Faces flow, which switches url on (Mojarra logs a WARNING, see Faces Flows).

Feature off, unless the application defines a flow, which switches url on (MyFaces logs at INFO).

none

Feature off. getClientWindow() returns null.

Same.

url

Window id in the jfwid URL parameter and in the hidden field jakarta.faces.ClientWindow. Implementation class com.sun.faces.lifecycle.ClientWindowImpl.

Same parameters. Implementation class org.apache.myfaces.lifecycle.clientwindow.UrlClientWindow.

url-redirect

Not supported: treated as none. A @ClientWindowScoped bean then fails with WELD-001303.

Documented by MyFaces as a value of the parameter. In the 4.1.4 test every plain GET was answered with a 302 to the same URL, see The MyFaces-Only Modes.

client

Not supported: treated as none.

Window id kept in the browser’s window.name, see The MyFaces-Only Modes.

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.name survives navigation. In url mode the same action starts a new window;

  • the URLs in the address bar are cluttered with jfwid and mfRid, and after an h:link click 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.

catalog.xhtml — every way of leaving the page
<!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>
edit.xhtml — a postback, a navigation, a redirect and an Ajax request
<!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>

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>
Table 4. Which requests carry the window id (url mode, both implementations)
Source of the request Id sent How

Postback of an h:form (h:commandButton, h:commandLink)

yes

In the query string of the form action and in the hidden field jakarta.faces.ClientWindow. The field wins when both exist.

h:link, h:button

yes

jfwid query parameter, added by the outcome-target renderer. The disableClientWindow attribute (value expression, Boolean) turns it off for one link.

Navigation outcome with faces-redirect=true

yes

The Location header of the 302 was /bookshelf/edit.xhtml?jfwid=W1 on both implementations.

Ajax request (f:ajax, faces.ajax.request)

yes

Part of the submitted form: the hidden field is posted with it.

Ajax response

yes

The partial response contains an update element for the hidden field, so the page keeps the right id after the update: <update id="j_id1:jakarta.faces.ClientWindow:0">W1</update> (the CDATA wrapper omitted).

h:outputLink

no

It renders its value as is. Add the id yourself or use h:link.

Hand-written <a href>, typed URL, bookmark

no

The request starts a new window, exactly as a new tab does.

Resource URLs (faces.js, stylesheets)

no

No client window is attached to resource requests.

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:commandLink or a navigation rule. A literal <a href> or h:outputLink silently starts a new window, and a @ClientWindowScoped bean 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 without jfwid on both implementations. The 4.1 VDL documents the attribute on h:link and h:button only.

  • The id is in the address bar only after the first Faces link or redirect. The first GET of a tab has no id in its URL; the server fabricates one and writes it into the page. After the user clicks an h:link, or after a redirect, the address contains the jfwid parameter. 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 the Edit form of the first tab with the first tab’s draft text, and the same BookEditor instance, 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:

WindowInfo.java — null-safe access
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.

js.xhtml — the window id in JavaScript
<!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:

BookEditor.java
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:

Table 5. Scenario run in one browser profile (instance numbers from the run)
Step Window BookEditor

Open catalog.xhtml (plain GET)

W1

new instance, no title

Click the Edit Effective Java command link, change the title to A draft, save

W1

same instance, title A draft

Follow h:link back to the catalog (the link carries jfwid=W1)

W1

same instance, the catalog shows title=A draft

Open catalog.xhtml again with a plain GET (second tab)

W2

another instance, no title

Follow the plain <a href="edit.xhtml"> from tab one

W3

a third instance, no title

Send a GET for edit.xhtml?jfwid=W1 after the session was invalidated

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 @PreDestroy method of BookEditor on invalidation. Mojarra did not: the log never showed destroyed BookEditor for a client window bean, in line with the source of its ClientWindowScopeContextManager, which only clears the bean maps when the session is destroyed. Do not rely on @PreDestroy of a @ClientWindowScoped bean 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.

Table 6. Server-side view state limits (shared by all tabs of a session)
Mojarra 4.1.17 MyFaces 4.1.4 Meaning

com.sun.faces.numberOfLogicalViews (15) with com.sun.faces.numberOfViewsInSession (15)

org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION (20)

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:

Table 7. Tab A is idle while tab B loads three pages (limit lowered to 2 for the test)
Step Mojarra (numberOfLogicalViews = 2) MyFaces (NUMBER_OF_VIEWS_IN_SESSION = 2)

Tab A loads edit.xhtml and saves once

200, title=A draft

200, title=A draft

Tab B loads edit.xhtml three times (three views)

200 each

200 each

Tab A saves again

500 ViewExpiredException: viewId:/edit.xhtml - View /edit.xhtml could not be restored.

500 ViewExpiredException - View "/edit.xhtml" could not be restored.

Tab A reloads its address edit.xhtml?jfwid=W1

200, title=A draft, same BookEditor instance

200, title=A draft, same BookEditor instance

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 ViewExpiredException on 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 BookEditor kept the draft after the view had expired. A page that reloads by GET on 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 = client the 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 unless com.sun.faces.disableClientStateEncryption is 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 ClientWindow directly. Its decode must make getId() 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.

SafeClientWindow.java
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;
    }
}
SafeClientWindowFactory.java
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);
    }
}
WEB-INF/faces-config.xml (fragment)
<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:

Table 8. Effect of SafeClientWindow
jfwid sent by the client Window used

my-own-id

my-own-id (well-formed)

ok:12

ok:12

"><script>alert(1)</script>

a generated UUID

an empty value

a generated UUID

x y (contains a space)

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: calling super.getId() again would return the unvalidated id of the wrapped window.

  • Do not return jfwid from getQueryURLParameters. 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 getQueryURLParameters returning Map.of("area", "books"), MyFaces produced ?area=books&jfwid=W1 in the form action and in links. Mojarra 4.1.17 produced ?jfwid=W1&area=books in h:link URLs but the malformed ?jfwid=W1=area=books in 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 @ClientWindowScoped bean instead of in additional URL parameters.

  • A decorated factory must still return null when the feature is off, because it receives that null from 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-id created a window with exactly that id and its own BookEditor. 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 (&quot;&gt;&lt;script&gt; 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 jfwid gets 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 SafeClientWindow of 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, HttpOnly and SameSite, and do not create sessions for anonymous requests that do not need one.

  • Bound the damage. jakarta.faces.NUMBER_OF_CLIENT_WINDOWS caps 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:outputLink and 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 and Referer headers.

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.

windows.sh
#!/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

Table 9. Differences observed for the client window
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 :0, then :1

Short token, consecutive within a session (for example wve5kxm2i, then wve5kxm2j)

Supported modes

none, url; anything else means none

none, url, url-redirect, client

url-redirect

treated as none

redirects forever in the test (the id is missing from the redirect URL)

client

treated as none

window id in window.name, first GET is a loader page

Flows with the parameter unset

switches url on, logs a WARNING; with an explicit none it overrides to url and warns

switches url on, logs at INFO; with an explicit none it keeps none

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

@PreDestroy of @ClientWindowScoped beans

never called (only cleared)

called when the session ends

Extra parameters from getQueryURLParameters

correct in links, malformed in the form action (?jfwid=W1=area=books)

correct everywhere

Encoding of the id in URLs

h:link and h:button percent-encode it (%3A); the form action carries it as is (: and spaces unencoded)

percent-encoded everywhere (%3A, + for a space)

Window id in the Ajax response

update element for the jakarta.faces.ClientWindow field

same

What Changed per Version

Version Change

2.2

The ClientWindow feature: ClientWindow, ClientWindowFactory, ClientWindowWrapper, Lifecycle.attachWindow, ExternalContext.getClientWindow() and setClientWindow, the hidden field and URL parameter, and the context parameter javax.faces.CLIENT_WINDOW_MODE with the values none and url. Faces Flows depend on it.

2.3

ClientWindowWrapper(ClientWindow wrapped), the constructor to use ("Since 2.3"); the no-argument constructor is deprecated.

3.0

The rename to jakarta.faces.*: jakarta.faces.lifecycle.ClientWindow and the parameter jakarta.faces.CLIENT_WINDOW_MODE.

4.0

@ClientWindowScoped (specification issue 1509) and jakarta.faces.NUMBER_OF_CLIENT_WINDOWS (both "Since 4.0"). The JavaScript library is now faces.js and the function is faces.getClientWindow.

4.1

No change in the ClientWindow API or the scope: the change log lists none.

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.name scripts were common workarounds in applications of that era, and Seam 2 has ended. Today the same job is the standard ClientWindow, and the window.name idea survives only in the MyFaces client mode.

  • javax names. Text and sample code for 2.2 and 2.3 use javax.faces.lifecycle.ClientWindow and javax.faces.CLIENT_WINDOW_MODE. Faces 4.x needs jakarta.faces.* (What Changed per Version).

  • None of the books covers ClientWindow or @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 @ClientWindowScoped bean.

Common Mistakes

  • Using a @SessionScoped bean for page state. Two tabs overwrite each other (The Problem: Several Tabs, One Session). Use @ViewScoped, @ClientWindowScoped or a flow.

  • Using @ClientWindowScoped without the mode. WELD-001303 on the first request. Set jakarta.faces.CLIENT_WINDOW_MODE to url.

  • Linking with a literal <a href> or h:outputLink. The link starts a new window and the bean starts over. Use h:link, h:button, h:commandLink or a navigation rule.

  • Testing with driver.get() for every page. Each call is a new window. Navigate with clicks or pass jfwid.

  • 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 @PreDestroy of @ClientWindowScoped beans on Mojarra. It is not called; MyFaces calls it.

  • A non-serializable @ClientWindowScoped bean. Deployment fails with WELD-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 @ClientWindowScoped state survives; reload the tab.

  • Returning jfwid from getQueryURLParameters or relying on extra parameters in Mojarra forms. See Custom Client Windows: ClientWindowFactory and ClientWindowWrapper.

  • Using the MyFaces url-redirect mode in 4.1.4. It redirected forever in the test.

  • Keeping a draft in @ClientWindowScoped beans for longer than the session. The state ends with the session. Persist what must survive.