State Saving and Stateless Views

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. Faces 4.0 behaves the same for everything below except where a section says otherwise; the differences between releases are collected in What Changed per Version. Every behaviour, size and error message on this page comes from a scratch Bookshelf WAR that was run on Apache Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and driven with scripted HTTP requests (checked on 2026-10-08). Where the two implementations disagree, Mojarra and MyFaces Compared lists the differences. Faces 5.0 is still a milestone release at the time of writing and nothing here describes it as current behaviour.

HTTP forgets everything between two requests, but a Faces page is a tree of stateful components: an input remembers the value the user typed, a button knows which listeners are attached, a table knows which row is being edited. Faces solves the mismatch by saving a description of the tree at the end of one request and restoring it at the start of the next. That description is the view state. Where it is kept, how big it is, what it may contain and what happens when it is lost decide a large part of how an application behaves under load, in a cluster and under attack. This page explains all of it, and ends with the one kind of view that has no state at all.

What the View State Is

The specification divides the state of a view in two pieces: the tree structure, meaning the parent-child relationships of the components and their facets, and the component state, meaning the attributes and properties of every component and the validators, converters and listeners attached to it. Together they are enough to rebuild the page the user is looking at, so that the next request can run the lifecycle against it. Three consequences follow from the definition.

  • The view state is not the model. The values of a backing bean live in the bean and in the bean’s scope (see Scopes). A @ViewScoped bean is kept in the session whichever way the view state is saved: in the runs below the component state travelled to the browser, and the bean still stayed in the session, so a state replayed in a new session got a new bean.

  • It is not the page source either. The Facelets page is the recipe for the initial tree and is applied again on a postback; what the view state adds is whatever differs from it (Partial State Saving, and the Deprecated Full State Saving).

  • It is per view, not per user. One session holds many views: one for every page the user opened, every tab, every reload.

The hidden field jakarta.faces.ViewState is how the browser hands the state, or a reference to it, back to the server. The specification fixes the name and strongly recommends that implementations make the value hard to predict. Each h:form renders one. This is what the Bookshelf draft form produced with each way of saving the state (identifiers shortened where noted):

The hidden field in the three modes (Mojarra 4.1.17 first, MyFaces 4.1.4 where it differs)
<!-- server state saving, the default: a reference of a few dozen bytes -->
<input type="hidden" name="jakarta.faces.ViewState" id="j_id1:jakarta.faces.ViewState:0"
       value="-9216585846328768218:-353409625333825011" autocomplete="one-time-code" />
<!-- MyFaces: id="j_id__v_0:jakarta.faces.ViewState:1" value="ZmM4MDBjNTY4YTY2YjFlYjAwMDAwMDAx" -->

<!-- client state saving: the state itself, compressed and encrypted, 1088 characters for this tiny form -->
<input type="hidden" name="jakarta.faces.ViewState" id="j_id1:jakarta.faces.ViewState:0"
       value="I0phbttbxIQf6OVxBoCVw5jkmXcznpY7QrYaqPpV7iOyDNbpplxvZAGw7LUqgoX6Eru2YfH54JCB+ArQ..." autocomplete="one-time-code" />

<!-- stateless view: the literal word, nothing is saved -->
<input type="hidden" name="jakarta.faces.ViewState" id="j_id1:jakarta.faces.ViewState:0"
       value="stateless" autocomplete="one-time-code" />

The figure below puts the three modes side by side: where the state lives, what the browser holds, what each costs and how each one fails.

Server state saving keeps a token in the browser and the state in the HTTP session, client state saving sends the whole encrypted state to the browser, and a stateless view saves nothing

A few details of the markup are worth knowing.

  • The autocomplete attribute is one-time-code on both implementations (the MyFaces configuration table notes that the default changed to it in 4.1.4). It tells the browser not to restore a stale state into the hidden field when the user returns with the back button, which several browsers ignore for the value off. Mojarra has com.sun.faces.autoCompleteOffOnViewState to render off instead, MyFaces has org.apache.myfaces.AUTOCOMPLETE_OFF_VIEW_STATE with the values one-time-code, true and false.

  • The id of the field is made of the view root’s container client id, the separator, the name and a number that is unique in the view. Mojarra can omit the id altogether with com.sun.faces.enableViewStateIdRendering (default true).

  • The hidden field comes at the end of the form on both implementations. com.sun.faces.writeStateAtFormEnd (default true) and org.apache.myfaces.RENDER_FORM_VIEW_STATE_AT_BEGIN (default false) move it to the beginning.

  • MyFaces adds a second hidden field, f_SUBMIT (the form id plus _SUBMIT), which it needs to recognise that the form was submitted; Mojarra adds a field named after the form id. A script that posts a form by hand has to send the one the implementation expects, plus the view state.

An Ajax request sends the field too, and the response brings the next value back as an ordinary update element, which faces.js writes into the field of every form of the page:

An Ajax response carries the new view state (Mojarra, server state saving)
<partial-response>
  <changes>
    <update id="f:out"><![CDATA[<span id="f:out">view=4 saves=1 title=abc</span>]]></update>
    <update id="j_id1:jakarta.faces.ViewState:0"><![CDATA[-3878530320989289415:-5334554848323205659]]></update>
  </changes>
</partial-response>

Where State Saving Sits in the Lifecycle

State is restored in the first phase and saved in the last one; the four phases in between work on the restored tree. The tutorial states the same: on a postback the view is rebuilt and "the saved state is applied".

Table 1. State saving in the six phases
Phase What happens to the state

Restore View

ResponseStateManager.isPostback looks for the jakarta.faces.ViewState parameter. If it is absent the request is initial and a new view is created. If it is present, isStateless is asked first: the value stateless means the view is rebuilt from the page and nothing is restored. Otherwise getState obtains the saved state (a lookup in the session, or decoding, verifying and deserialising the client value), the component tree is rebuilt from the Facelets page and the saved state is applied to it. If the state cannot be found or verified the phase ends in a ViewExpiredException (ViewExpiredException).

Apply Request Values to Invoke Application

The restored tree is live: converters, validators, listeners and the model update run against components whose attributes and local values are the ones the user saw. Components the actions add or remove are part of the state saved at the end.

Render Response

After the view is rendered the state is collected (StateManagementStrategy.saveView), and either stored in the session with a token written to the page, or encrypted and written into the page. A stateless view writes only the word stateless.

The classes that take part are layered so that each hides one decision from the layer above. The page author sees none of them.

Table 2. The key players of state management
Class Role

ViewHandler and ViewDeclarationLanguage

The entry points. ViewDeclarationLanguage.restoreView and writeState are called by the lifecycle; getStateManagementStrategy lets the view technology take over the work.

StateManagementStrategy

Saves and restores the view by visiting the tree. Mojarra and MyFaces ship a partial and a full variant (the API names FaceletPartialStateManagementStrategy and FaceletFullStateManagementStrategy as the known subclasses).

StateManager

The abstraction of "where". isSavingStateInClient, getViewState and writeState are what is left of it in 4.x.

ResponseStateManager

The render kit’s part: writes the hidden field, reads it back, decides isPostback and isStateless, and for client state saving encrypts and verifies in a tamper-evident way.

StateHolder, PartialStateHolder, StateHelper

What a component implements to take part (State in Components, Including Your Own). Every UIComponent already does.

The postback restore is the part most worth seeing once:

sequenceDiagram participant B as Browser participant S as FacesServlet participant V as ViewHandler participant R as ResponseStateManager B->>S: POST with jakarta.faces.ViewState S->>V: Restore View V->>R: isPostback R-->>V: true, the field is present V->>R: isStateless alt the value is stateless V->>V: build the view from the Facelets page else a saved state V->>R: getState R-->>V: the state, found in the session or decrypted V->>V: build the view, then apply the saved state end S->>S: phases 2 to 5 run on the restored tree S->>V: Render Response V->>R: writeState R-->>B: HTML with the next jakarta.faces.ViewState

The Web Forms comparison of The Same Idea in ASP.NET Web Forms maps the same round trip onto ASP.NET, and the lifecycle page (Restore View) describes the phase itself.

Server State Saving

Server state saving keeps the state in the HTTP session and sends the browser only a token. The specification names the jakarta.faces.STATE_SAVING_METHOD context parameter, with the values server and client (case is ignored), and says that server is the default when it is not set; the tutorial says the same in its instructions for choosing where state is saved. Both implementations behaved like that. (The Javadoc of StateManager.STATE_SAVING_METHOD_PARAM_NAME says the default is client, which contradicts the specification text and what both implementations do; trust the specification.)

web.xml — server state saving, written out, with the limits the next sections explain
<context-param>
    <param-name>jakarta.faces.STATE_SAVING_METHOD</param-name>
    <param-value>server</param-value>
</context-param>

<!-- Mojarra: 15 logical views x 15 states each (the defaults), written out -->
<context-param>
    <param-name>com.sun.faces.numberOfLogicalViews</param-name>
    <param-value>15</param-value>
</context-param>
<context-param>
    <param-name>com.sun.faces.numberOfViewsInSession</param-name>
    <param-value>15</param-value>
</context-param>

<!-- MyFaces: the latest 20 views (the default), and 4 sequential ones -->
<context-param>
    <param-name>org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION</param-name>
    <param-value>20</param-value>
</context-param>
<context-param>
    <param-name>org.apache.myfaces.NUMBER_OF_SEQUENTIAL_VIEWS_IN_SESSION</param-name>
    <param-value>4</param-value>
</context-param>

How it works, as observed:

  • The first render saves, the postback looks up. The state of a view is stored in the session under a token, and the token is the value of the hidden field. The token is not the state: on Mojarra it is two numbers joined by a colon (the first identifies the logical view, that is, the initially requested page and all its postbacks, and stays constant across them; the second identifies the saved state and changes with every request), on MyFaces it is a Base64 text made of a random part and a counter. The format is an implementation detail; do not parse it.

  • The token only works in the session that issued it. A postback that carried the token but not the session cookie, and a postback from another session, both failed in Restore View with a ViewExpiredException on both implementations. The session is therefore part of the security of server state saving: with Mojarra’s com.sun.faces.generateUniqueServerStateIds (default true) the token is a random number instead of a counter, so an attacker who can make a browser submit a chosen token cannot name a particular stored view, and the value no longer reveals how many views the session has built.

  • The state is an object graph, not bytes, unless asked. By default the saved state stays in the session as a Java object graph and is not serialised until the container serialises the session (to disk, or for replication). jakarta.faces.SERIALIZE_SERVER_STATE=true (Faces 2.2) serialises it at once, which costs a serialisation and a deserialisation per request and buys only one thing: a NotSerializableException shows up in the request that causes it (What Must Be Serialisable). com.sun.faces.compressViewState then compresses what goes into the session on Mojarra, and org.apache.myfaces.COMPRESS_STATE_IN_SESSION does it on MyFaces.

  • Old states stay usable for a while. Posting from an old page works as long as its state is inside the window of the limits below, which is what keeps the back button usable.

The Limits of the Session

The session would grow without bound if every view stayed. Both implementations keep a bounded number and evict the oldest. The specification names none of the settings; they belong to the implementations, and this is the same table that the scopes page uses for the view scope, because evicted state and view-scoped beans are tied.

Table 3. Settings that bound the states saved in a session
What it limits Mojarra 4.1.17 MyFaces 4.1.4

Logical views kept in the session (one per initially requested page, whatever the number of its postbacks)

com.sun.faces.numberOfLogicalViews, default 15

no such concept

States kept for each logical view (the back-button window of one page)

com.sun.faces.numberOfViewsInSession, default 15; with the previous setting, up to 225 states per session

org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION, default 20, the latest views of the whole session

Sequential postbacks of one page that are kept before older ones are dropped

covered by the setting above

org.apache.myfaces.NUMBER_OF_SEQUENTIAL_VIEWS_IN_SESSION, default 4

The behaviour was checked on both implementations. A page was loaded and then posted back repeatedly, each postback made from the page the previous one returned, which produces a chain of states; then the form was submitted again from one of the older states (a fresh session and a fresh chain for each probe, so that a probe does not evict the next one).

Table 4. Re-posting from older states of one page
Setting Mojarra 4.1.17 MyFaces 4.1.4

Limit of 2 (numberOfLogicalViews and numberOfViewsInSession, or NUMBER_OF_VIEWS_IN_SESSION), chain of five states

the first three states expired (ViewExpiredException, HTTP 500), the next one was processed

the same

Defaults, chain of 21 states (20 postbacks)

states 0 to 5 expired, states 6 to 20 (the latest 15) were processed

states 0 to 16 expired, states 17 to 20 (the latest 4) were processed, although NUMBER_OF_VIEWS_IN_SESSION is 20

MyFaces with NUMBER_OF_SEQUENTIAL_VIEWS_IN_SESSION set to 10, chain of 9 states

not applicable

every state was processed

So the back-button window of a single page is 15 states on Mojarra and, by default, 4 on MyFaces: the sequential limit of MyFaces applies to consecutive postbacks, the view limit to distinct views. A user who clicks Save five times and then goes back five pages with the browser’s back button meets the exception on MyFaces with its defaults. What exhausts the limits in practice, however, is not postbacks but new views: every GET, every reload, every tab and every crawler request creates a view. The experiment of the scopes page showed it: with numberOfLogicalViews and numberOfViewsInSession set to 2, four GET requests made the postbacks of the first two pages fail. The limits are per session, not per browser window, so a user who opens many tabs of the catalogue can expire the form in the first tab, which the multi-window page explains from the window side. When the application needs several tabs, raise the limits (the cost is memory) or use a way of saving state that does not occupy the session.

Mojarra keeps view-scoped beans under a separate limit (com.sun.faces.numberOfActiveViewMaps), so on Mojarra the beans of an evicted state may outlive it; MyFaces destroys them together with the state. The details are in the scopes page.

Client State Saving

Client state saving writes the whole state into the page and keeps nothing on the server. Setting it is one parameter:

web.xml — client state saving
<context-param>
    <param-name>jakarta.faces.STATE_SAVING_METHOD</param-name>
    <param-value>client</param-value>
</context-param>

The specification requires the state written to the client to be encrypted in a tamper-evident way, and the Javadoc of ResponseStateManager.writeState repeats the requirement for client state saving. Both implementations do it by default. The pipeline, read from the sources and confirmed by switching compression and encryption off one at a time, is: serialise the state with Java serialisation, compress it, encrypt it with AES, authenticate it with an HMAC (SHA-256 on both) and encode it as Base64.

Table 5. The size of the hidden field for the same tiny draft form (one input and five buttons), in characters
Configuration Mojarra 4.1.17 MyFaces 4.1.4

Server state saving: the token

39

32

Client state, partial state saving, defaults (encrypted; Mojarra compresses by default, MyFaces does not)

1088

704

Client state with compression switched on (MyFaces) or off (Mojarra): org.apache.myfaces.COMPRESS_STATE_IN_CLIENT or com.sun.faces.compressViewState

2008

600

Client state with encryption switched off (never in production, see below)

1012

656

Client state with full state saving (Partial State Saving, and the Deprecated Full State Saving)

3564

11520

The figures are single readings and vary by a few characters from request to request, because the identifiers inside the state change. The numbers grow with the tree: a page with a large table or a long form carries tens of kilobytes in every response and in every request. The state travels in the body of the request for postbacks, which is fine, but a state that large costs a client with a slow uplink on every click, and the CPU of compressing and encrypting it falls on the server instead of the memory of the session.

What client state saving does, as observed on both implementations:

  • It does not need the session. A postback with the state and without the session cookie was processed. Closing the session, timing out or failing over to another node does not expire the page (the view-scoped bean is new, because beans stay in the session).

  • It survives a restart only when the key is fixed. A state saved before a restart was rejected afterwards when no key was configured and accepted when one was (Keys: Where They Come From and Where They Must Not).

  • It is not bound to the session or the user. A valid state taken from one session was accepted in another. The value is self-contained; the unpredictability that server state saving gets from the token is absent, so do not use the field as a request-forgery token (the security page covers protection against forged requests).

  • A modified or invented state is rejected, but not always with the same exception (next table).

Table 6. A state that cannot be verified, client state saving (every case is an HTTP 500 unless an error page handles it)
Posted state Mojarra 4.1.17 MyFaces 4.1.4

a valid state with one character changed in the middle

ViewExpiredException (the message says the view could not be restored)

ViewExpiredException

a short invented value (AAAA)

FacesException caused by an ArrayIndexOutOfBoundsException while decrypting

ViewExpiredException

an invented value with server state saving (bogus, 1:2)

ViewExpiredException

IllegalArgumentException for text that is not valid Base64, ViewExpiredException for a well-formed token that is unknown

The Javadoc of ResponseStateManager.getState says that a state which fails to decrypt or shows evidence of tampering must raise a ProtectedViewException; neither implementation does. A single error page for ViewExpiredException covers the tampered case on both, and a second one for the generic FacesException, or a filter in front of the servlet, covers the rest.

Timeouts

The age of a client state can be limited: com.sun.faces.clientStateTimeout on Mojarra and org.apache.myfaces.CLIENT_VIEW_STATE_TIMEOUT on MyFaces, both in minutes and active only when set. With the value 1, a state that was 70 seconds old was still accepted, and one that was 130 seconds old was rejected with a ViewExpiredException on both implementations, so treat the value as a minimum lifetime, good to within about a minute, not as a precise timer. Mojarra’s source counts whole minutes and compares with a strict greater-than, which is why the first minute after the nominal timeout still passes.

Keys: Where They Come From and Where They Must Not

Encryption and authentication need keys, and the keys decide the operational behaviour of client state saving.

Table 7. Where each implementation takes its keys
Aspect Mojarra 4.1.17 MyFaces 4.1.4

Source of the key

the JNDI environment entry java:comp/env/faces/ClientSideSecretKey, a Base64 string of an AES key

the context parameters org.apache.myfaces.SECRET (the encryption key, Base64) and org.apache.myfaces.MAC_SECRET (the key of the HMAC, Base64), two separate keys

If none is configured

a random key per application start; only a FINEST log message says so

a random key per application start (INFO: No context init parameter 'org.apache.myfaces.SECRET' found.)

Key sizes accepted in the runs

16 and 32 bytes

16 and 32 bytes

Algorithms

AES in CBC mode with PKCS5 padding and a random IV per state, HmacSHA256 over IV and ciphertext, as the source of the guard shows

org.apache.myfaces.ALGORITHM (default AES), ALGORITHM.PARAMETERS (default ECB/PKCS5Padding), ALGORITHM.IV (required for CBC, and used as given for every state), MAC_ALGORITHM (default HmacSHA256)

Switching encryption off

com.sun.faces.disableClientStateEncryption=true

org.apache.myfaces.USE_ENCRYPTION=false

Four rules follow, and each is the opposite of something that is easy to do.

  • A key is configuration, never source. No key appears in web.xml, in faces-config.xml or in a class. Read it from the environment of the process or from a secret store, at deployment time. The example below does it for Tomcat.

  • Every node of a cluster and every restart must use the same key, or the state of a page saved by one node cannot be restored by another, and every open page expires at every deployment. Without a configured key that is exactly what happens: in the runs, a state saved before a restart failed after it with a ViewExpiredException on both implementations, and worked after it when a key was configured.

  • On Mojarra, a session key can win over the configured key. When a session exists, Mojarra’s ByteArrayGuard reads the key from the session attribute com.sun.faces.SK and stores its configured key there if the attribute is empty. With a replicated session, node B therefore decrypts the state saved by node A with the key of node A, whatever B was configured with: wrong keys on a cluster are masked for existing sessions and surface only for new or cookie-less requests. MyFaces has no such mechanism. The run that showed it is in the deployment and clustering page; do not rely on it, configure the same key everywhere.

  • Changing the key expires every open page. Rotate keys deliberately, at a quiet moment, and keep the old key out of the configuration afterwards; there is no key ring.

  • Do not switch encryption off to look at the state. The unencrypted state is a Java serialisation stream and the server deserialises what it receives. On both implementations the setting drops the authentication code together with the encryption (the state in the runs began with the gzip or serialisation header and carried no code in front of it), so the server would deserialise bytes chosen by the client, a well-known way to attack Java applications. For debugging, use the development stage and the server-side logging instead.

Tomcat can substitute environment variables into the files it parses when it is told to, which is enough to keep a key out of the web application:

META-INF/context.xml for Mojarra — the key comes from the environment variable FACES_CLIENT_SECRET
<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <!-- Read from the process environment when Tomcat parses this file; the file holds a placeholder only. -->
    <Environment name="faces/ClientSideSecretKey" type="java.lang.String"
                 value="${FACES_CLIENT_SECRET}" override="false"/>
</Context>
META-INF/context.xml for MyFaces — the keys come from the environment
<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Parameter name="org.apache.myfaces.SECRET" value="${MYFACES_SECRET}" override="false"/>
    <Parameter name="org.apache.myfaces.MAC_SECRET" value="${MYFACES_MAC_SECRET}" override="false"/>
</Context>
Generating the keys and starting Tomcat with environment substitution enabled
# Generate once, store in the secret manager of the platform, never in the repository.
openssl rand -base64 32          # one value per variable

export FACES_CLIENT_SECRET="$(read-from-your-secret-store faces-client-secret)"   # placeholder command
export JAVA_OPTS="-Dorg.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource"
"$CATALINA_HOME/bin/catalina.sh" run

Both examples were run with the embedded Tomcat of the scratch application: a state saved before a restart was accepted after it with the key in the environment (16 and 32 byte keys), and rejected with another key. Two failure modes are worth knowing.

  • Without the property source, Tomcat does not substitute the variable and the literal text ${FACES_CLIENT_SECRET} becomes the key. Mojarra logged SEVERE: Unexpected exception initializing encryption. No encryption will be performed. and then failed to render every page with a NullPointerException; MyFaces failed with an HTTP 500 too. Neither wrote an unprotected state, so the failure is loud, which is what you want.

  • Application servers have their own way of exposing environment variables or secrets to a JNDI entry or a context parameter (expression resolution in the server’s descriptors, vault integrations, container secrets). Use that mechanism and run the same restart test: save a page, restart, post it.

The name of the Mojarra JNDI entry changed in Faces 4.0. Mojarra 2.2.20 and 3.0.0 read java:comp/env/jsf/ClientSideSecretKey, and the 4.0 and 4.1 releases read java:comp/env/faces/ClientSideSecretKey. An application that moves from 3.0 to 4.x with the old entry in its server configuration silently falls back to a random key per start: it still works until the first restart or the second node. When the Mojarra runtime has no java.naming module, as in a minimised runtime image, Mojarra does not set up the encryption at all (its documentation says that encryption is only set up when JNDI entries can be processed); include the module and test.

Checking What Your Application Really Sends

The first bytes of the decoded value tell whether a state is protected. The gzip header (Base64 H4sI) or the Java serialisation header (Base64 rO0AB) at the start of the field means the state is readable, compressed or not and unencrypted. Encrypted values look like random text. Run it against a test deployment of every application before release:

audit-viewstate.sh — fails when a client state looks like gzip or Java serialisation
#!/bin/bash
# usage: audit-viewstate.sh https://test.example.org/bookshelf/catalogue.xhtml
url="$1"
state="$(curl -s "$url" | grep -o 'name="jakarta.faces.ViewState"[^>]*value="[^"]*"' | sed 's/.*value="//; s/"$//')"
if [ -z "$state" ]; then
  echo "no jakarta.faces.ViewState field found"; exit 2
fi
case "$state" in
  stateless)            echo "stateless view: nothing to protect" ;;
  H4sI*|rO0AB*)         echo "FAIL: client state is not encrypted (readable serialisation)"; exit 1 ;;
  *)
    echo "state length: ${#state} characters"
    if [ "${#state}" -lt 100 ]; then echo "looks like a server state token"; else echo "opaque: encrypted client state"; fi ;;
esac

A Mojarra state with disableClientStateEncryption set began with H4sIAAAAAAAA and decoded to a gzip stream whose payload started with the serialisation magic aced0005; the MyFaces state with USE_ENCRYPTION=false began with rO0AB.

What is encrypted is still sent to the user’s browser. Everything the components hold is inside the field, so the same rule as for every client-side store applies: never put a secret, a credential or a personal record in an attribute or property of a component just because the browser cannot read the encrypted value, and prefer the server state if the page holds sensitive data.

Choosing Between Server and Client State

Neither is better. They move the cost: from the session to the network and the CPU, and from the server’s memory to the user’s browser.

Table 8. Server state saving and client state saving compared
Aspect Server state saving Client state saving

Memory of the server

grows with the open views and tabs of every session, bounded by the limits of The Limits of the Session

none for the state; view-scoped beans still live in the session

Size of responses and requests

a token of a few dozen bytes

the whole state, from about a kilobyte for a tiny form to tens of kilobytes for a large page, in every response and every postback

CPU

none by default; serialisation only when asked

serialise, compress, encrypt and authenticate on every request, and the reverse on every postback

Session timeout or eviction

the page expires (ViewExpiredException)

the page keeps working, subject to the optional state timeout

Cluster

sticky sessions or session replication; the state must be Serializable to replicate

any node can restore the page if every node has the same key

Restart or redeploy

sessions are usually lost, so are the pages

pages survive only with a fixed key

Back button and old tabs

only the states inside the window of the limits

every old page still works until the state timeout

Security

the session is the boundary; the token is random and tied to the session

the state is attacker-controlled input protected only by the key, and replayable; never unencrypted

What operations must provide

session sizing, replication if clustered

keys in a secret store, the same in every node, a rotation procedure

A practical order of preference:

  • Keep server state saving, the default, and size the limits for how users work, until a measurement shows a problem.

  • When the problem is a page that expires because the user leaves it open longer than the session lives, ask first whether the page should be a stateless view (Stateless Views), whether the session timeout is right, and whether the form should save a draft.

  • Choose client state saving when the state is small, the nodes of the cluster do not share sessions, or session memory is the bottleneck, and only with fixed keys, compression and a restart test in the deployment pipeline.

  • A page that does not need the tree to persist at all does not need either: a stateless view has nothing to size, expire or secure.

Partial State Saving, and the Deprecated Full State Saving

Before Faces 2.0 every request saved all of the tree: the structure and the state of every component, including everything the page author wrote as attributes. Partial state saving, introduced in 2.0 and the behaviour since, rebuilds the tree from the Facelets page at the start of every postback and saves only the delta: what each component changed after the view was built. Faces calls the moment after the first build the initial state (UIComponent.markInitialState, initialStateMarked and clearInitialState of PartialStateHolder); anything set on a component later, by an action, a listener or a programmatic change, is part of the delta, and anything the tag handlers set while building the view is not.

The effect is large, and the size table of Client State Saving shows it: the same form needed 1088 characters with partial state saving on Mojarra and 3564 with full state saving, and 704 against 11520 on MyFaces.

Two consequences matter for the page author.

  • The view is rebuilt on every postback, so what decides the tree must be the same on the postback. Tags that decide at build time (c:if, c:forEach, ui:include with an expression for src) are evaluated again, against the model as it is now; if the answer changed, the tree differs from the one the state was saved for. The page on build time and render time (the postback bug) shows the symptoms. Mojarra offers com.sun.faces.restoreBuildTimeDecisions (4.0.23, default false), which saves the result of each decision and replays it, and com.sun.faces.refreshTransientBuildOnPSS (4.0.19, default false), which reapplies the page before rendering; MyFaces offers org.apache.myfaces.REFRESH_TRANSIENT_BUILD_ON_PSS (default auto) and org.apache.myfaces.REFRESH_TRANSIENT_BUILD_ON_PSS_PRESERVE_STATE. Each costs more work per postback and papers over a design that should use render-time tags (rendered, ui:repeat) instead.

  • Programmatic changes are saved if they go through the component API. Setting an attribute or property in an action, adding a child in a listener or switching rendered off are all part of the delta (State in Components, Including Your Own).

Full state saving is the older algorithm. It stays available through two settings, jakarta.faces.PARTIAL_STATE_SAVING=false for the whole application and jakarta.faces.FULL_STATE_SAVING_VIEW_IDS for a comma separated list of view ids. Faces 4.1 deprecates both, and the StateManager constants (PARTIAL_STATE_SAVING_PARAM_NAME, FULL_STATE_SAVING_VIEW_IDS_PARAM_NAME) are marked deprecated for removal since 4.1, with the stated reason that full state saving is to be dropped in favour of partial state saving, to keep the specification simple. The two implementations warn at startup:

WARNING: The configuration 'jakarta.faces.PARTIAL_STATE_SAVING' is deprecated as of Faces 4.1 and should not longer be used.

That is Mojarra’s wording, typo included; MyFaces prints the same message up to Faces 4.1. Use full state saving only for a legacy component that cannot be made to cooperate with partial state saving, set it for the single view that needs it, plan its removal and note that when the application does not declare the parameter, the specification asks the runtime to derive the mode from the version of the application: applications declared as version 1.2 or lower must not use partial state saving.

A scripted check of the ids: with Mojarra, jakarta.faces.FULL_STATE_SAVING_VIEW_IDS set to /draft.xhtml saved that view in full (a 3564-character client state against 1088) and the form worked on postback. With MyFaces 4.1.4 the same value, /draft.xhtml, enlarged the state (9708 characters against 704) but the postback of the listed view then failed in Restore View with IllegalStateException: Error restoring component: j_id__v_0, and the value without the slash was ignored. Treat the per-view setting of MyFaces as unreliable in 4.1.4 and test it before relying on it. jakarta.faces.PARTIAL_STATE_SAVING=false worked on both.

web.xml — the settings of this section, for reference only (do not add them to a new application)
<!-- Deprecated since 4.1: back to the pre-2.0 algorithm for the whole application -->
<context-param>
    <param-name>jakarta.faces.PARTIAL_STATE_SAVING</param-name>
    <param-value>false</param-value>
</context-param>

<!-- Deprecated since 4.1: only these views are saved in full (MyFaces 4.1.4: test before use) -->
<context-param>
    <param-name>jakarta.faces.FULL_STATE_SAVING_VIEW_IDS</param-name>
    <param-value>/legacy/report.xhtml,/legacy/export.xhtml</param-value>
</context-param>

Two cheap settings reduce the cost of every state save. com.sun.faces.disableIdUniquenessCheck (default false) skips the walk that verifies that every component id is unique inside its naming container; true drops one full tree walk per state save in every project stage, auto drops it only in Production. MyFaces has org.apache.myfaces.CHECK_ID_PRODUCTION_MODE (default auto). Leave them at their defaults during development, because a duplicate id is a bug you want to see.

State in Components, Including Your Own

Everything above is automatic for the standard components. A custom component takes part by keeping its state where the framework can see it. The specification asks a component that extends UIComponent directly to implement PartialStateHolder (or StateHolder); the helper StateHelper does it for you, and UIComponentBase returns one from getStateHelper(). The custom components page of this section (its state section) describes the four ways to hold a value. The component below uses the two that matter most for state saving, and the table after it shows what each state saving mode does to them.

ShelfFlag.java — one saved property and one plain field
package org.example.bookshelf.component;

import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.UIOutput;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;
import java.io.IOException;

@FacesComponent(value = "bookshelf.ShelfFlag", createTag = true,
        namespace = "urn:bookshelf:state", tagName = "flag")
public class ShelfFlag extends UIOutput {

    enum PropertyKeys { highlight }

    private String scratch = "none";                       // a plain field: not part of the state

    @Override
    public String getFamily() {
        return "bookshelf.state";
    }

    public boolean isHighlight() {
        return (Boolean) getStateHelper().eval(PropertyKeys.highlight, false);   // saved with the view
    }

    public void setHighlight(boolean highlight) {
        getStateHelper().put(PropertyKeys.highlight, highlight);
    }

    public String getScratch() {
        return scratch;
    }

    public void setScratch(String scratch) {
        this.scratch = scratch;
    }

    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        ResponseWriter out = context.getResponseWriter();
        out.startElement("span", this);
        out.writeAttribute("id", getClientId(context), null);
        out.writeAttribute("class", isHighlight() ? "flag flag-on" : "flag", null);
        out.writeText(getValue() + " [highlight=" + isHighlight() + ", scratch=" + scratch + "]", null);
        out.endElement("span");
    }
}
flag.xhtml and the action that changes the component (flags.xhtml is the same page with a transient view)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:s="urn:bookshelf:state">
<f:view transient="false">
    <h:head><title>Flag</title></h:head>
    <h:body>
        <h:form id="f">
            <s:flag id="flag" value="Dune"/>
            <h:commandButton id="set" value="Set" action="#{requestDraft.flag}"/>
            <h:commandButton id="other" value="Other" action="#{requestDraft.save}"/>
        </h:form>
    </h:body>
</f:view>
</html>
// in the backing bean: change the component in one request ...
public void flag() {
    FacesContext context = FacesContext.getCurrentInstance();
    ShelfFlag flag = (ShelfFlag) context.getViewRoot().findComponent("f:flag");
    flag.setHighlight(true);                       // StateHelper
    flag.setScratch("set in a previous request");  // plain field
}
// ... and look at it in the next one (any other button of the form)
Table 9. What the component showed after clicking Set, and after the next postback (identical on both implementations)
Mode After Set (same response) After the next postback

Server state, partial state saving

highlight=true, scratch=set in a previous request

highlight=true, scratch=none

Client state, partial state saving

the same

the same: highlight=true, scratch=none

Full state saving

the same

the same: highlight=true, scratch=none (the plain field is lost without saveState and restoreState)

Stateless view

highlight=true, scratch=set in a previous request

highlight=false, scratch=none: both were lost

The property survives because the StateHelper records the change as part of the delta. The plain field resets on every request because the component is created again from the page on every postback. If a component must save an extra field by hand, it overrides saveState and restoreState and calls the superclass first, which the specification stresses is essential, and saveState must not change the state. State that must never be saved, because it is rebuilt on every request, goes in the TransientStateHelper (getTransientStateHelper().putTransient). Components in a stateless view lose the first kind too, which is why the specification warns that a component that needs state to operate correctly must not be used in a stateless view.

What Must Be Serialisable

The specification states the rule for the state of the whole view, whichever way it is saved: the values of all component attributes and properties, and the saved state of attached objects, must implement Serializable such that writing the aggregate state to an ObjectOutputStream does not throw a NotSerializableException. For attached objects (converters, validators, listeners, behaviours) it distinguishes three kinds:

  • an object that implements StateHolder saves itself;

  • an object that is Serializable is saved with Java serialisation;

  • an object that is neither must have a public no-argument constructor and is restored only to its initial default state.

Whether the rule is enforced when the state is saved depends on the mode, and the failure usually shows up late. The check below put an object into the view root’s attributes, which are part of the view state, with an action:

DraftBean.java — an action that puts the wrong kind of object into the state (never do this)
public void poison() {      // not Serializable and without a public no-argument constructor
    FacesContext.getCurrentInstance().getViewRoot().getAttributes().put("junk", new Handle("cover-service"));
}

public void poison2() {     // declared Serializable, but holds a field that is not
    FacesContext.getCurrentInstance().getViewRoot().getAttributes().put("junk", new Leaky());
}

public static class Handle {
    private final String name;
    public Handle(String name) { this.name = name; }
}

public static class Leaky implements Serializable {
    private final Object connection = new Object();
}
Table 10. What each mode did with the two objects
Mode Handle (not serialisable, no default constructor) Leaky (serialisable class, non-serialisable field)

Server state, not serialised (the default)

the request succeeded; the next postback failed in Restore View, because the restore tried to recreate the object. Mojarra: IllegalStateException caused by NoSuchMethodException: ...Handle.<init>(). MyFaces: IllegalStateException: Error restoring component, caused by a RuntimeException whose message names the missing no-argument constructor

worked, in the same JVM; it would fail when the session is serialised for replication or passivation

SERIALIZE_SERVER_STATE=true

the same failure on the next postback

Mojarra: the request that caused it failed in Render Response with java.io.NotSerializableException: java.lang.Object. MyFaces: the response was rendered, SEVERE: Exiting serializeView - Could not serialize state: java.lang.Object was logged, and the next postback failed with ViewExpiredException because no state was stored

Client state

the same failure on the next postback

both implementations: the request that caused it failed in Render Response with NotSerializableException

The lesson is the one the specification gives in 7.8.2: serialisation is optional for server state, and an application that works with the default may break in a cluster. Run the development and test environments with jakarta.faces.SERIALIZE_SERVER_STATE=true (and Mojarra’s com.sun.faces.enableClientStateDebugging=true, which deserialises a client state right after writing it) so that the mistake fails in the request that makes it; never run production with the debugging parameter. The usual causes are UIComponent or FacesContext references, entity managers, streams and other infrastructure objects stored in component attributes; keep identifiers, and look infrastructure up when it is needed.

Stateless Views

Some pages have no use for a persistent tree: a search box, a listing that is rebuilt from its parameters on every request, a login form. A stateless view tells Faces not to save any state for it. Faces 2.2 added it, through the transient attribute of f:view, and the specification describes it as a view whose component state is not saved, to be used with full awareness of the statefulness requirements of its components.

quick-search.xhtml — the Bookshelf search box as a stateless view
<!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">
<f:view transient="true">
    <h:head>
        <title>Quick search</title>
    </h:head>
    <h:body>
        <h:form id="search">
            <h:inputText id="query" value="#{quickSearch.query}" required="true"/>
            <h:commandButton id="go" value="Search" action="#{quickSearch.search}"/>
            <h:messages id="messages"/>
        </h:form>
        <ul>
            <ui:repeat value="#{quickSearch.results}" var="title">
                <li>#{title}</li>
            </ui:repeat>
        </ul>
    </h:body>
</f:view>
</html>
QuickSearch.java — a request scoped bean, which is what a stateless view wants
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;
import java.util.List;
import java.util.Locale;

@Named("quickSearch")
@RequestScoped
public class QuickSearch {

    private static final List<String> TITLES =
            List.of("Dune", "Dune Messiah", "Emma", "Neuromancer", "Persuasion");

    private String query = "";
    private List<String> results = List.of();

    public String getQuery() {
        return query;
    }

    public void setQuery(String query) {
        this.query = query;
    }

    public List<String> getResults() {
        return results;
    }

    public void search() {
        String needle = query.strip().toLowerCase(Locale.ROOT);
        results = TITLES.stream()
                .filter(title -> title.toLowerCase(Locale.ROOT).contains(needle))
                .toList();
    }
}

The attribute is a jakarta.el.ValueExpression that must evaluate to a Boolean; if it is true the implementation calls setTransient(true) on the UIViewRoot, and "this view must not participate in state saving or restoring". The attribute is evaluated when the view is built, on every request including the postback, and the postback of an h:form is sent to the view’s URL without its query string (a page requested with ?x=1&y=2 rendered action="/bookshelf/quick-search.xhtml" on both implementations), so an expression that depends on a request parameter would give another answer on the postback. Use a constant or something the postback repeats.

What the runs showed, on both implementations:

  • The field says stateless. The view state is the literal word, in the page and in the Ajax response’s update element. ResponseStateManager.isStateless is how Restore View recognises it, and UIViewRoot.isTransient() reads the flag in code.

  • No session is needed. A GET of the page created no session (Mojarra set no cookie at all; MyFaces set only its flash token cookie, oam.Flash.RENDERMAP.TOKEN). A postback from a client that had no cookie was processed: the search for dune returned Dune and Dune Messiah. The view is a rebuild from the page, so there is nothing for the session to hold.

  • Conversion, validation and messages work, because they belong to the lifecycle, not to the saved state: the empty required field produced Validation Error: Value is required., and Ajax requests with f:ajax worked too.

  • A POST without jakarta.faces.ViewState is an initial request. The action did not run and the page was rendered fresh. A hand-written form that posts to a stateless view has to include the field, with the value stateless.

  • Components that an action changed are rebuilt from the page on the next request. The ShelfFlag of State in Components, Including Your Own, and a style attribute that an action set on an output text, showed the change in the response of the action and had lost it on the next request, on both implementations. A change must be made again in every request, or the view must be stateful.

  • @ViewScoped beans do not work usefully. The specification does not require them to work in a stateless view; each request created a new instance. The scopes page (Stateless Views) shows the difference between the two implementations. A request-scoped bean, or a session-scoped bean for what must survive, is the match.

  • Nothing guards the form against forgery. There is no per-view token, so any client that can reach the URL can post to a stateless view with the literal value. State-changing actions need the container’s or the application’s own protection (the security page), which is one more reason to keep stateless views to reads and low-risk forms.

Table 11. When a stateless view fits and when it does not
Good fit Poor fit

Search, filters and listings that read their parameters from the request or from the query string

Wizards, editable tables, any page whose tree is changed by actions or listeners

Login and contact forms with request scoped beans

Pages that use @ViewScoped beans, binding to components kept between requests or dynamically added components

Public pages that must scale without sessions

Forms with Ajax interactions that depend on what the component held in the previous request

A form rendered in a cluster whose nodes do not share sessions

Pages that rely on f:viewParam with required="true": on a postback of a transient view the parameter is missing, so the validation fails (repeat the query on the post URL, or add a hidden input with the same name)

ViewExpiredException

The exception means that the state the postback refers to is not available. It is thrown in Restore View, before any of the application’s code runs, so it cannot be caught by a bean; it reaches the container as an exception.

Table 12. Why a view expires
Cause Notes

The session ended (timeout, logout, invalidate) while the page was open

Server state saving only. A very common cause: a user leaves a form open longer than the session timeout.

The state was evicted

Server state saving only. Too many views or tabs for the limits of The Limits of the Session; or too many postbacks and a back button.

The server restarted or the application was redeployed

Server state: sessions lost. Client state: the key changed, unless it is fixed (Keys: Where They Come From and Where They Must Not).

A cluster node without the session or without the key received the postback

Missing sticky session or replication; different keys.

The state was modified, truncated or invented

Client state; also for an unknown token with server state saving.

The client state timeout has passed

Only when clientStateTimeout or CLIENT_VIEW_STATE_TIMEOUT is set.

What the user sees is the default error page of the server unless the application handles the exception. The simplest handler is an error page in web.xml that tells the user what happened and offers the way forward:

web.xml and expired.xhtml — a friendly page for an expired view
<error-page>
    <exception-type>jakarta.faces.application.ViewExpiredException</exception-type>
    <location>/expired.xhtml</location>
</error-page>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html">
<h:head><title>Page expired</title></h:head>
<h:body>
    <h1>Your page expired</h1>
    <p>For your security the page was closed while you were away. Reload the catalogue and try again.</p>
    <h:link outcome="catalogue" value="Back to the catalogue"/>
</h:body>
</html>

In the runs the page was served with the status 500 on both implementations, and the handler worked on Mojarra in every project stage. MyFaces used the error page in the Production stage and its own detailed error page in Development, so an application that tests the handler only in development on MyFaces does not see it. An Ajax request does not reach the error page at all: the response is an ordinary HTTP 200 whose body is a partial-response with an error element, class jakarta.faces.application.ViewExpiredException on Mojarra and jakarta.faces.application.ViewExpiredException on MyFaces, which is why a client script has to look for the name with includes, not equality (Ajax Error Handling). The exception handling page covers ExceptionHandler and the OmniFaces FullAjaxExceptionHandler, which turn an expired view into a navigation, including for Ajax.

Handling the exception is not the same as preventing it. The measures, cheapest first: make the page a stateless view if it can be; size the session limits for the number of tabs; set the session timeout to the real idle time of the users; save a draft of long forms on the server; use client state saving with fixed keys when sessions are the problem. A bean cannot hide an expired view, and neither should it, because the state it refers to is gone: the user must reload.

Reading and Observing the State Mode in Code

The 4.1 API for state is small; most of what the old StateManager offered was removed in 4.0 (What Changed per Version).

Table 13. The state classes and methods that remain
API Use

StateManager.isSavingStateInClient(FacesContext)

true when the application saves state in the client. Application.getStateManager() returns the instance.

StateManager.getViewState(FacesContext)

the view state as a string, without markup (this saves the view; calling it costs a state save)

UIViewRoot.isTransient() and setTransient(boolean)

whether the view is stateless; what f:view transient sets

ResponseStateManager.VIEW_STATE_PARAM

the name of the hidden field, jakarta.faces.ViewState

ResponseStateManager.isStateless(FacesContext, String)

used by the lifecycle; true if the preceding writeState was stateless

ResponseStateManager.getCryptographicallyStrongTokenFromSession(FacesContext)

a strong token for protected views and the like

StateManagerWrapper, StateManagementStrategy

extension points of an implementation or a library (not for applications)

StateHolder, PartialStateHolder, StateHelper, TransientStateHelper

what components and attached objects implement (State in Components, Including Your Own)

The implementations add service providers for the serialisation: Mojarra reads com.sun.faces.serializationProvider, a class name of a com.sun.faces.spi.SerializationProvider, which is called on every state save and restore; MyFaces reads org.apache.myfaces.SERIAL_FACTORY, a factory for serialising the view state. Both are for special needs (a faster serialiser, a class loader trick); the default is correct for applications.

A diagnostic bean for the development stage is enough to see the mode of a deployment. It reads the state mode and shows whether the current view is transient:

StateInfo.java — the state mode of the current request
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;

@Named("stateInfo")
@RequestScoped
public class StateInfo {

    public boolean isClient() {
        FacesContext context = FacesContext.getCurrentInstance();
        return context.getApplication().getStateManager().isSavingStateInClient(context);
    }

    public boolean isViewTransient() {
        return FacesContext.getCurrentInstance().getViewRoot().isTransient();
    }
}
<h:outputText value="state saved in the #{stateInfo.client ? 'client' : 'server'}, transient view: #{stateInfo.viewTransient}"/>

What State Saving Costs, and How to Measure It

The cost of state saving is a function of the number of components, so everything that shrinks the tree shrinks the cost: fewer components per page, ui:repeat or a data table over a page of rows instead of the whole list, and rendered on regions that are not needed. The performance page collects the measures; the ones specific to state saving are these.

  • Measure the field. With client state saving, the length of jakarta.faces.ViewState is the number of characters that every response and every postback of the page carries. The script below prints it for any page.

  • Know what the state contains. Large values kept in component attributes or properties, and tables whose rows keep their own component state, each cost bytes (the rowStatePreserved attribute makes a table or repeat save the whole state of the components of every row, which its Javadoc says adds overhead).

  • Compress client state. Mojarra does by default, MyFaces needs org.apache.myfaces.COMPRESS_STATE_IN_CLIENT=true; in the runs it reduced the 704-character field to 600.

  • Keep the session lean for server state. One state per postback and per view; raise the limits only as far as users need.

  • Tune the serialiser last. The serialisation providers of Reading and Observing the State Mode in Code are only worth touching when a measurement says that serialisation is the cost.

state-size.sh — the size of the view state of a page
#!/bin/bash
# usage: state-size.sh https://test.example.org/bookshelf/catalogue.xhtml
curl -s "$1" \
  | grep -o 'name="jakarta.faces.ViewState"[^>]*value="[^"]*"' \
  | sed 's/.*value="//; s/"$//' \
  | awk '{ print length($0) " characters" }'

State Saving in a Cluster

The deployment and clustering page covers the topic as a whole. From the state saving point of view the options are four, and each has a precondition that is easy to forget.

Table 14. Clustering options and what each requires
Option Requires Failure if it is missing

Server state with sticky sessions

a load balancer that routes a session to one node

a postback reaches a node without the state: ViewExpiredException

Server state with session replication

every component attribute and property Serializable; test with SERIALIZE_SERVER_STATE=true

NotSerializableException when the session is replicated, not when the state is saved

Client state

the same fixed keys on every node (Keys: Where They Come From and Where They Must Not)

ViewExpiredException for every page saved by another node on MyFaces; on Mojarra with a replicated session the key kept in the session attribute com.sun.faces.SK can mask different configured keys, and new or cookie-less requests fail instead

Stateless views

request scoped beans, no reliance on state

none for state; the application must not need it

The specification also tells implementers that want saved state to fail over to another container instance to keep that in mind in their server side strategy, and notes that serialising the view into the session lets the usual session failover mechanisms of the container do the work.

The Same Idea in ASP.NET Web Forms

ASP.NET Web Forms solved the same problem with the same trick, a hidden field that round-trips the state of the control tree, and the two descriptions complement each other. The Web Forms side is written up in ViewState and Control State, its place in the page life cycle in Page Life Cycle and Postback, the session-based alternative in State Management and Caching and the key material in Security.

Table 15. View state in Faces and in ASP.NET Web Forms
Idea Jakarta Faces ASP.NET Web Forms

The hidden field

jakarta.faces.ViewState, one per form

__VIEWSTATE, plus __EVENTVALIDATION

Where the state lives by default

on the server, in the session; the field holds a token. Client state saving is a switch.

in the page: the field holds the state. A session based persister (SessionPageStatePersister) is an alternative.

What is saved

the delta of the components against the view built from the page (partial state saving); the tree is rebuilt from the page

the changed properties of the controls whose state tracking is on, matched by control id and position; control state is saved separately

Opting out

f:view transient="true" for a whole view; UIComponent.setTransient(true) for a component in code

EnableViewState="false" and ViewStateMode per page or control

Integrity and confidentiality

only for client state: AES with an HMAC, keys from JNDI (Mojarra) or context parameters (MyFaces)

a MAC (EnableViewStateMac) and optional encryption, keys from machineKey

Tying the state to the user

the token of server state is random and works only in the session that created it

ViewStateUserKey, set by the page

A farm of servers

sticky sessions or replication for server state; the same keys everywhere for client state

the same machineKey on every server

When the state is not there

ViewExpiredException, thrown in Restore View

a state that fails verification is rejected before it is deserialised (the Web Forms page explains how)

Measuring the cost

the length of the field; the browser’s network tools

the length of __VIEWSTATE; Page.Trace

Three differences are worth keeping in mind when moving between the two. First, the default is opposite: Faces keeps the state on the server and Web Forms sends it to the browser, so the first problem of a Faces application is a page that expires, and the first problem of a Web Forms page is its weight. Second, Faces has a limit on how many views a session keeps and Web Forms has none, because the browser is the store. Third, in Faces client state saving moves the exact Web Forms risks to the application: the keys (machineKey there, the JNDI entry or the context parameters here) must be the same on every node, secret and kept out of source control, and a state that cannot be verified must be rejected. The stateless view has no Web Forms equivalent as a single switch; the nearest thing is ViewStateMode="Disabled" on a page, with the difference that a Faces stateless view rebuilds the tree on every request by design.

Mojarra and MyFaces Compared

Everything in this table was observed in the same application on Tomcat 11.0.26 and Weld 6.0.4.

Table 16. State saving differences between the implementations
Aspect Mojarra 4.1.17 MyFaces 4.1.4

Default state saving method

server

server

Server state token

two numbers joined by a colon, the first stable across the postbacks of one page

Base64 of a random part and a counter

Limits on the session

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

NUMBER_OF_VIEWS_IN_SESSION (20) and NUMBER_OF_SEQUENTIAL_VIEWS_IN_SESSION (4)

A chain of 20 postbacks of one page, then re-posting from each state

states 6 to 20 (15 states) were processed, older ones expired

states 17 to 20 (4 states) were processed, older ones expired

SERIALIZE_SERVER_STATE with a non-serialisable object in the state

the request fails in Render Response with NotSerializableException

the response is rendered, SEVERE: Exiting serializeView - Could not serialize state is logged, the next postback expires

Client state key

JNDI java:comp/env/faces/ClientSideSecretKey (3.0 and 2.x: jsf/)

context parameters org.apache.myfaces.SECRET and org.apache.myfaces.MAC_SECRET

Client state without a configured key

random key per start; restart expires all pages

random key per start; restart expires all pages

Client state compression

on by default (com.sun.faces.compressViewState)

off by default (org.apache.myfaces.COMPRESS_STATE_IN_CLIENT)

Encryption algorithm

AES/CBC/PKCS5Padding with a random IV, HmacSHA256 (source)

org.apache.myfaces.ALGORITHM (AES) with ALGORITHM.PARAMETERS (default ECB/PKCS5Padding), HmacSHA256

Tampered client state

ViewExpiredException

ViewExpiredException

Garbage (AAAA) as client state

FacesException caused by ArrayIndexOutOfBoundsException

ViewExpiredException

Malformed token with server state (bogus)

ViewExpiredException

IllegalArgumentException (invalid Base64)

error-name of an expired view in an Ajax response

class jakarta.faces.application.ViewExpiredException

jakarta.faces.application.ViewExpiredException

error-page for ViewExpiredException

used in Development and Production

used in Production; Development shows the MyFaces error page

Stateless view and the session

no cookie set by the GET

only the oam.Flash.RENDERMAP.TOKEN cookie, no session

Per-view full state saving (FULL_STATE_SAVING_VIEW_IDS)

worked

/draft.xhtml enlarged the state and broke the postback in 4.1.4

The two bundled implementations are not interchangeable for state saving settings: every context parameter above that starts with com.sun.faces or org.apache.myfaces is ignored by the other implementation. An application that is meant to run on both (the servers page lists what each server bundles) sets both families, and runs the restart and re-post tests on each.

What Changed per Version

Version Change

1.x

Server and client state saving, selected by STATE_SAVING_METHOD. The whole tree was saved for every request.

2.0

Partial state saving becomes the default; StateHelper, PartialStateHolder and StateManagementStrategy are introduced.

2.2

Stateless views: the transient attribute of f:view and ResponseStateManager.isStateless. jakarta.faces.SERIALIZE_SERVER_STATE (then javax.faces.). ClientWindow for several tabs.

3.0

The package rename to jakarta.faces.*, including jakarta.faces.STATE_SAVING_METHOD. The Mojarra JNDI entry of the client state key is still jsf/ClientSideSecretKey.

4.0

The deprecated StateManager methods are removed: saveSerializedView, saveView, restoreView, writeState(FacesContext, SerializedView) and the class StateManager.SerializedView, and ResponseStateManager.getTreeStructureToRestore, getComponentStateToRestore and writeState(FacesContext, SerializedView). Mojarra reads the key from java:comp/env/faces/ClientSideSecretKey. Mojarra 4.0.19 and 4.0.23 add refreshTransientBuildOnPSS and restoreBuildTimeDecisions.

4.1

Full state saving is deprecated for removal: jakarta.faces.PARTIAL_STATE_SAVING and jakarta.faces.FULL_STATE_SAVING_VIEW_IDS (specification issue 1829). MyFaces 4.1.4 changes the default autocomplete of the field to one-time-code.

5.0 (upcoming)

Faces 5.0 is a milestone release; this page does not describe it. The deprecation of full state saving suggests the settings above will go; check the release notes before relying on them.

Where the Books Are Outdated

  • The StateManager API. Burns and Schalk describe how to write a custom StateManager for JSF 1.0 and 1.1 with saveSerializedView, getTreeStructureToSave, getComponentStateToSave and a SerializedView, and the older books show getTreeStructureToRestore and getComponentStateToRestore of the ResponseStateManager. These methods are deprecated since the 2.x line and removed in Faces 4.0 (checked against the 3.0.0 and 4.0.1 API jars). The view is saved and restored by StateManagementStrategy, and StateManager.getViewState and ResponseStateManager.getState remain.

  • An unencrypted client state shown as normal. The same book shows a client state field named com.sun.faces.VIEW whose value starts with H4sI, which is the Base64 of a gzip stream: readable, unauthenticated state. The field is called jakarta.faces.ViewState, the defaults of both implementations encrypt and authenticate it, and the audit of Checking What Your Application Really Sends treats a value that starts with H4sI as a failure.

  • Everything is saved. The older books explain that the whole component tree is serialised into the session or the page. Since Faces 2.0 the tree is rebuilt from the Facelets page and only the delta is saved, which changes both the size of the state and the rules for components that change after build time (Partial State Saving, and the Deprecated Full State Saving). Full state saving is the way they describe it and is now a deprecated setting.

  • Client state saving as a casual switch. Mann, Dudney and others, and Jacobi and Fallows show javax.faces.STATE_SAVING_METHOD with the value client or server and move on. The name is jakarta.faces.STATE_SAVING_METHOD since 3.0, and none of them mentions the Mojarra key setting, the need for fixed keys in a deployment, or the rule that client state without encryption and a MAC must never be sent to a browser (Keys: Where They Come From and Where They Must Not). Mann’s advice to make sure that an application works with both settings, because the person who configures production may change the parameter, still holds, and the restart and re-post tests of this page are how to follow it.

  • State saving advice that belongs to JSP views. Mann recommends developing with client state so that a changed JSP page cannot clash with a view restored from the session, and Jacobi and Fallows explain that the f:view end tag calls writeState. JSP is gone (removed in 4.0); Facelets rebuilds the view from the page at every postback, and the state is written by the view declaration language.

  • Component state by hand. Mann and Jacobi and Fallows show saveState and restoreState returning an Object array, which is valid and is still required for fields that are not in the StateHelper. Faces 2.0 added the StateHelper, which current components use, and the rules for attached objects (StateHolder, Serializable or a public no-argument constructor) matter more now that partial state saving restores components from the page (State in Components, Including Your Own).

  • Seam-era workarounds. Leonard’s cookbook switches to client state saving to make a Seam conversation propagate. Seam 2 is no longer maintained; use CDI scopes and the standard conversation and flow scopes.

  • No stateless views. None of the books covers f:view transient="true", which arrived in Faces 2.2.

  • Session memory as free. The books treat server state as unbounded. Both implementations limit the number of views and states kept per session (The Limits of the Session), and MyFaces keeps only four sequential states by default.

Common Mistakes

  • Client state saving without a fixed key. It works until the first restart or the second node, and then every open page expires. Set the key from the environment and test a restart.

  • Client state saving without encryption and a MAC. disableClientStateEncryption=true or USE_ENCRYPTION=false left on from a debugging session sends a Java serialisation stream that the server will deserialise. Audit the field (Checking What Your Application Really Sends).

  • A key in web.xml, faces-config.xml, a class or the repository. Read it from the environment or a secret store; rotate it when it leaks.

  • Treating the view state as a CSRF token. With client state it is replayable and not bound to the session; with a stateless view it is a constant. Protect state-changing requests separately.

  • Putting secrets or personal data into component attributes with client state. The browser holds the state. It is encrypted, but it is not yours.

  • Assuming server state is unlimited. Many tabs, many reloads and crawlers fill the limits; the form in the first tab expires. Size the limits and prefer stateless views for read-only pages.

  • Expecting the back button to work after many postbacks on MyFaces. The default keeps four states of a chain of postbacks.

  • Storing non-serialisable objects in component attributes. Works in a single JVM and fails at replication time, or on the next postback. Test with jakarta.faces.SERIALIZE_SERVER_STATE=true.

  • Using @ViewScoped beans in a stateless view. A new instance per request.

  • Changing a component in an action of a stateless view. The change is gone on the next request.

  • Relying on the f:view transient expression to read a request parameter. The postback does not repeat the query string.

  • Turning on full state saving to fix a rebuild bug. It hides the symptom, costs three to sixteen times the state in the runs and is deprecated for removal. Use render-time tags.

  • No handler for ViewExpiredException. The user sees a server error page. Add the error page, and handle the Ajax case in the client.

  • Testing the error page on MyFaces in Development only. MyFaces shows its own page there; test in Production.

  • Upgrading Mojarra from 3.0 to 4.x with the key under jsf/. The new name is faces/; the old entry is ignored and the key is random again.