WebSocket Push with f:websocket
|
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, which lists Jakarta
WebSocket 2.2 as an optional dependency of the Faces specification), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4,
Java 17 or later (the examples were compiled with --release 21), Jakarta Servlet 6.1 and Jakarta WebSocket 2.2.
f:websocket and @Push exist since JSF 2.3, so almost everything below also applies to Faces 4.0; the differences are
collected in What Changed by Version. Every example was 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. Pushes were received by scripted WebSocket clients (Node.js 24) and
by a headless Chrome page driven through the DevTools protocol; every message, close code and error text quoted below was
observed in those runs on 2026-10-08. Claims taken only from the documentation are marked as such.
An HTTP request is always started by the browser. A page that has to show "a new review just arrived" has three options:
ask again and again (polling), keep one request open (long polling or Server-Sent Events), or open a WebSocket, a
long-lived full-duplex connection defined by RFC 6455 on which the server can send a frame whenever it wants. Faces wraps
the WebSocket option in one tag, f:websocket, and one injectable object, PushContext. The design is deliberately
small: the channel is one-way (server to client), and everything the browser has to send back goes through ordinary
Faces Ajax, which keeps the view state, the HTTP session and the security constraints that a raw WebSocket message would
not carry (see the partial lifecycle).
How It Works
The pieces, in the order they appear:
-
The context parameter
jakarta.faces.ENABLE_WEBSOCKET_ENDPOINTregisters the server endpoint (Enabling the Endpoint). -
The tag
f:websocketcreates ajakarta.faces.component.UIWebsocketcomponent. It renders no markup, only a<script>that callsfaces.push.init(...)with a URL of the form/jakarta.faces.push/<channel>?<generated id>(The f:websocket Tag). -
The JavaScript API
faces.pushinfaces.jsopens the socket, reconnects it, decodes the JSON and calls your listeners (The Client Side). -
The CDI side:
@Push PushContextinjected anywhere sends a message to every socket open on that channel (The Server Side: @Push and PushContext). -
Scopes and users decide who receives the message (Scopes and Users).
Enabling the Endpoint
The endpoint is off by default. Enable it in web.xml:
<context-param>
<param-name>jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT</param-name>
<param-value>true</param-value>
</context-param>
Source: Jakarta EE Tutorial: Using WebSockets with Jakarta Faces Technology and Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters
The constant is PushContext.ENABLE_WEBSOCKET_ENDPOINT_PARAM_NAME. If the servlet container runs its WebSocket
container on a different TCP port than HTTP, the optional integer parameter jakarta.faces.WEBSOCKET_ENDPOINT_PORT
(PushContext.WEBSOCKET_ENDPOINT_PORT_PARAM_NAME) tells Faces which port to put in the generated URL:
<context-param>
<param-name>jakarta.faces.WEBSOCKET_ENDPOINT_PORT</param-name>
<param-value>8000</param-value>
</context-param>
Source: Jakarta Faces 4.1 API: Push
What happens when it is missing differs between the implementations (both observed):
| Implementation | Result of a page with f:websocket and no ENABLE_WEBSOCKET_ENDPOINT |
|---|---|
Mojarra 4.1.17 |
The page fails at render time with the error |
MyFaces 4.1.4 |
The page renders normally and contains the |
Prerequisites by runtime. A server that implements the platform (WildFly, GlassFish, Payara, Open Liberty, TomEE in their full configurations) provides the
WebSocket container and JSON-P, so only the parameter is needed. Check that the server’s profile actually includes Jakarta WebSocket and JSON-P. Tomcat has the WebSocket container but not JSON-P, and
Mojarra encodes every message with Jakarta JSON Processing. On Tomcat 11.0.26 plus Weld with Mojarra 4.1.17 the first
PushContext.send(...) failed with java.lang.NoClassDefFoundError: jakarta/json/Json (thrown in the sender, the page
itself rendered fine; the frame that a client receives once the jars are present is {"data":...}) until jakarta.json:jakarta.json-api and an implementation (org.eclipse.parsson:parsson) were
added to WEB-INF/lib. MyFaces 4.1.4 on the same Tomcat pushed without jakarta.json or Parsson in the WAR; the frame it
sends is a bare JSON string (the encoded message itself, without the data wrapper that Mojarra’s frame has). So the two
implementations differ here: Mojarra needs JSON-P on Tomcat or Jetty, MyFaces did not in this test, and a WAR that must run
on both, or that may be switched from one to the other, should carry JSON-P anyway. A client that parses the frame itself, rather than
through faces.push, has to cope with both frame shapes. Both observations are from one run each on that Tomcat. See
Tomcat and Weld and Jakarta EE servers for the rest of such a setup; the Maven WAR setup lists the baseline dependencies.
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.eclipse.parsson</groupId>
<artifactId>parsson</artifactId>
<version>1.1.7</version>
</dependency>
Source: Jakarta Faces 4.1 specification (optional Jakarta WebSocket dependency) and Jakarta WebSocket 2.2
The f:websocket Tag
f:websocket is in the jakarta.faces.core namespace (xmlns:f="jakarta.faces.core"). Component type
jakarta.faces.Websocket, class jakarta.faces.component.UIWebsocket (a ClientBehaviorHolder, which is what lets
a nested f:ajax listen to the pushed message, see Updating the UI from a Push: f:ajax and h:commandScript), renderer
com.sun.faces.renderkit.html_basic.WebsocketRenderer in Mojarra. Attributes, as documented in the 4.1 VDL:
| Attribute | Required | Meaning |
|---|---|---|
|
yes |
The channel name. It may not be an EL expression and may only contain alphanumeric characters, hyphens, underscores and periods. All open sockets on the same channel (and, for |
|
no |
|
|
no |
A |
|
no |
JavaScript function called when the socket opens. Argument: the channel name. |
|
no |
JavaScript function called for every message. Arguments: the decoded message, the channel name, the raw |
|
no |
Called when a connection error occurred and the socket will try to reconnect. Arguments: the close code, the channel name, the raw |
|
no |
Called when the socket is closed and will not reconnect. Arguments: the close code, the channel name, the raw |
|
no |
Boolean, default |
|
no |
Boolean, default |
|
no |
Standard component identifier and component binding. The component’s client id is what |
UIWebsocket rejects invalid configuration when the page is rendered. Observed:
| Mistake | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
|
Error page whose message is just |
Same |
|
|
Accepted; renders a URL containing the space. Use only the allowed characters anyway. |
|
|
|
|
Rejected with the invalid-scope message, which ends with the hint that with |
Accepted; the page rendered with a plain init script |
The same channel name used with two scopes (for example |
|
No error observed |
The Client Side
Listeners
The listener attributes take the name of a function (without parentheses) or an inline function expression:
<f:websocket channel="reviews" onmessage="showReview"/>
<f:websocket channel="reviews" onmessage="function (review) { console.log(review); }"/>
The documented life cycle of a socket:
-
The socket is opened when the document loads and stays open as long as the document is open. It is closed implicitly when the document is unloaded.
-
When a successfully connected socket is broken (network error, server restart) it reconnects automatically at increasing intervals. It does not reconnect when the very first connection attempt fails.
-
onopenruns on the very first connection attempt, whether or not it succeeds, and not again after an automatic reconnect. -
onerrorruns on every failed connection of an already established socket, while it still intends to reconnect. It is not called on the final close. -
oncloseruns when the socket will not reconnect: the first attempt failed, the server closed it with 1000 (normal closure) or 1008 (policy violated), or the maximum number of reconnects was exceeded. The code-1means the browser has no WebSocket support,1000a normal close (the session or view expired), anything else an error. See RFC 6455 section 7.4.1 andCloseReason.CloseCodes.
Observed with listeners that logged into the page: onopen fired once, faces.push.close(...) produced onclose with
code 1000, and invalidating the HTTP session produced a server-side close with code 1000 on a view-scoped channel (Mojarra
sent the reason text Expired), on Mojarra and on MyFaces. The reconnect intervals and the server-restart behaviour were not run; they come from the
documentation.
The 4.1 JavaScript documentation of faces.push.init says onopen receives "the client identifier", while the VDL
says "the channel name". In both implementations onopen received the channel name (OPEN reviews in the log of the
test page), as the VDL says.
Define the Listeners Before the Tag
f:websocket renders its faces.push.init(...) call inline, at the place where the tag stands, and passes the
listener functions by reference. A listener defined in a <script> after the tag does not exist yet when that call runs:
<f:websocket channel="reviews" onopen="onOpen" onmessage="showReview"/>
<script>
function onOpen(channel) { /* ... */ }
function showReview(review) { /* ... */ }
</script>
On Mojarra this page logged ReferenceError: onOpen is not defined and no socket was initialised. MyFaces rendered the same
page without error (it resolved the functions later). Put the listeners in <h:head>, in a script resource loaded
with h:outputScript, or before the tag, as the Bookshelf page below does. The same ordering rule applies to the
name of an h:commandScript used as listener (Updating the UI from a Push: f:ajax and h:commandScript).
The Message Argument
The message is the Java object you passed to send, encoded as JSON and decoded by the script, so the listener
receives a JavaScript string, number, array or object. Always treat it as data, never as markup: write it with
textContent (or a template that escapes), not with innerHTML, because review text is entered by users. The
Bookshelf listener below passes the comment <b>Great</b> read through a push and it showed as literal text on both
implementations. See Security.
The Server Side: @Push and PushContext
jakarta.faces.push.Push is a CDI qualifier (@Qualifier, targets method, field and parameter) and
jakarta.faces.push.PushContext the injectable object. Inject it into any container-managed artefact, a managed bean, a
servlet, an observer or an EJB:
@Inject
@Push
private PushContext reviews; // channel name = the field name: "reviews"
@Inject
@Push(channel = "alerts") // channel name given explicitly
private PushContext notifications;
Source: Jakarta Faces 4.1 API: Push
The channel name is taken from the name of the injection target unless channel is given. This is the most common
mistake: a field called reviewsChannel sends to a channel reviewsChannel, nobody listens on it and nothing happens
and nothing fails. It happened in the scratch app (the page listened on reviews, the push went into the void with
HTTP 200 and no log line). Name the field after the channel or always write @Push(channel = "...").
PushContext (a Serializable interface) has three send methods, each returning Future<Void> handles for the
individual socket writes, which you can ignore or use to detect delivery problems:
| Method | Meaning |
|---|---|
|
To every socket open on the channel (within the scope rules). |
|
To the sockets whose |
|
To several users; the map holds the futures per user. |
The Message Is Encoded as JSON
The object is converted by a small JSON encoder inside the implementation. Supported shapes (all verified on both):
String, numbers, Boolean, null, arrays, collections, maps and JavaBeans (public getters). Because the encoder
differs, so do the results for less common types:
| Message content | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
Envelope on the wire |
|
The JSON of the object itself |
JavaBean with getters |
Object, properties sorted alphabetically; |
Object with the same property order, but |
|
ISO-8601 UTC string, |
RFC 1123 string, |
|
ISO strings ( |
A nested object of bean properties |
|
|
A nested object of bean properties (an enum became |
|
|
|
A |
|
|
The portable rule is therefore: send a String, a number, a Map<String, Object> of those, or a small JavaBean whose
getters return only strings, numbers and booleans, and format dates and enums yourself (toString() or an ISO
formatter). Do not send entities, records or anything with a long getter graph: besides the failures above, a bean’s
whole property graph ends up on the wire, including fields the user must not see.
Bookshelf: A Live Review Feed
The catalogue shows a review form. Every new review should appear at once on every open catalogue page, without reloading. Four small classes and one page do it. The message type is a JavaBean (not a record, see above):
package org.example.bookshelf.model;
import java.io.Serializable;
/** What the browser receives: a plain JavaBean with getters, so Faces can turn it into JSON. */
public class ReviewPush implements Serializable {
private final String title;
private final String reviewer;
private final int rating;
private final String comment;
public ReviewPush(Review review) {
this.title = review.title();
this.reviewer = review.reviewer();
this.rating = review.rating();
this.comment = review.comment();
}
public String getTitle() { return title; }
public String getReviewer() { return reviewer; }
public int getRating() { return rating; }
public String getComment() { return comment; }
}
The service stores the review and announces it with a CDI event; it knows nothing about WebSockets (see CDI events in backing beans):
@ApplicationScoped
public class ReviewService {
private final List<Review> reviews = new CopyOnWriteArrayList<>();
@Inject
private Event<ReviewSaved> saved;
public void save(Review review) {
reviews.add(review); // in a real application: persist inside a transaction
saved.fire(new ReviewSaved(review));
}
public List<Review> findAll() {
return List.copyOf(reviews);
}
}
public record ReviewSaved(Review review) { }
Source: Jakarta Faces 4.1 API: Push and Jakarta CDI 4.1 specification, Transactional observer methods
One observer turns the event into a push. Observing with TransactionPhase.AFTER_SUCCESS pushes only after a
surrounding transaction commits, so no page shows a review that was rolled back; when no transaction is active the CDI
observer is called immediately, which is what happened on Tomcat:
@ApplicationScoped
public class LiveReviews {
@Inject
@Push(channel = "reviews")
private PushContext reviews;
public void onSaved(@Observes(during = TransactionPhase.AFTER_SUCCESS) ReviewSaved event) {
reviews.send(new ReviewPush(event.review()));
}
}
The form’s backing bean only saves (the @Push injection stays out of the UI layer):
@Named
@RequestScoped
public class ReviewForm {
private String reviewer;
private int rating = 5;
private String comment;
@Inject
private ReviewService service;
public String submit() {
service.save(new Review("978-0-13-468599-1", "Effective Java", reviewer, rating, comment));
comment = null;
return null;
}
// getters and setters omitted
}
And the page. The two functions are declared in <h:head>, before the tag. The listeners use textContent:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<h:head>
<title>Reviews, live</title>
<script>
function showReview(review) {
const item = document.createElement("li");
item.textContent = review.reviewer + " rated " + review.title + " "
+ review.rating + "/5: " + review.comment;
document.getElementById("live-reviews").prepend(item);
}
function liveStatus(text) {
document.getElementById("live-status").textContent = text;
}
</script>
</h:head>
<h:body>
<h1>Reviews, live</h1>
<p id="live-status">connecting...</p>
<ul id="live-reviews"></ul>
<h:form id="reviewForm">
<h:messages/>
<h:inputText id="reviewer" value="#{reviewForm.reviewer}" required="true"/>
<h:inputTextarea id="comment" value="#{reviewForm.comment}" required="true"/>
<h:commandButton id="post" value="Post review" action="#{reviewForm.submit}">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
</h:form>
<f:websocket channel="reviews"
onopen="function () { liveStatus('live'); }"
onmessage="showReview"
onerror="function (code) { liveStatus('reconnecting (' + code + ')'); }"
onclose="function (code) { liveStatus('offline (' + code + ')'); }"/>
</h:body>
</html>
Source: Jakarta EE Tutorial: Using WebSockets with Jakarta Faces Technology and Jakarta Faces 4.1 API: Push
In the headless Chrome run the page showed live after loading; posting the form with the comment <b>Great</b> read added
<li>Ana rated Effective Java 5/5: <b>Great</b> read</li> to the list through the push (not through the Ajax
response, which renders only the form) on Mojarra and on MyFaces. Open the page in two windows and the second one gets the
review as well. Because the channel is application scoped (the default), everybody with the page open receives it,
signed in or not. That is right for a public catalogue and wrong for anything private (Scopes and Users).
Two practical details seen in the runs:
-
The server registers a socket slightly after the browser reports it open. A message sent in the same millisecond as
onopen(for example a test that sends from inside theonopenlistener) was lost on both implementations; waiting half a second made it arrive. A push that must not be missed needs a state snapshot when the page loads, and live pushes only for what happens afterwards: render the existing reviews from the database in the page, push only new ones. -
Push is not storage. A client that was offline misses messages; reconnecting does not replay them. The database is the source of truth.
Scopes and Users
A channel’s scope decides which sockets it addresses and who may send:
scope |
Receivers | Who can send |
|---|---|---|
|
Every socket open on the channel name anywhere in the application, in any session. |
Anything with the |
|
The sockets on the channel name within the current HTTP session (all tabs of one user). |
Code running for that user. Per the documentation, not the application. |
|
Only the socket of the current view; another tab on the same URL does not receive it. |
Code running for that view, typically an action or Ajax listener in the same view. Not the application. |
|
The sockets of that user, found by the user identifier, whichever session they are in. |
Anything: other users, the application, background jobs. |
Session and View Scope Bind to the Injection Context
For session and view scope Mojarra resolves "the current session" and "the current view" when the PushContext
is created, and keeps the result. A request-scoped bean that is injected and used inside a Faces request therefore
sends to the right view. The same injection in an @ApplicationScoped bean is created once and stays bound to whatever
session or view happened to be current at that moment. Observed on a fresh Mojarra server with a view-scoped channel:
the first user’s click on a button calling an application-scoped sender updated that user’s page; the second user’s
click, in another session, did not (the Ajax listener on the second page never ran). MyFaces delivered both. The cause is
visible in the Mojarra source: WebsocketPushContext stores the session and view channel maps in its constructor. So:
-
use
applicationscope or user-targeted messages (below) from application-scoped beans and background jobs; -
for
sessionandviewscope inject thePushContextinto a bean with a matching or shorter life (request, view, session) and send while that request is running.
The PushContext of a view-scoped channel injected into a request-scoped bean and called from an Ajax listener worked on
both implementations (the push arrived in the same view, the nested f:ajax below ran).
Addressing Users
Set user to the authenticated user’s name and send with the user as second argument:
<f:websocket channel="alerts" user="#{request.remoteUser}"
onmessage="function (text) { showAlert(text); }"/>
@ApplicationScoped
public class Alerts {
@Inject
@Push
private PushContext alerts;
public void notifyUser(String text, String userName) {
alerts.send(text, userName);
}
public void notifyUsers(String text, Collection<String> userNames) {
alerts.send(text, userNames);
}
}
With three clients registered as alice, bob and carol, send(text, "alice") reached only alice’s socket, and
send(text, List.of("alice", "carol")) reached those two, on both implementations. A plain send(text) on that
channel reached nobody: once a channel is declared with user, it has no application-wide audience.
Where the user value comes from matters. The scratch page took the user from a request parameter to be able to simulate
three users; with that page, anybody can open ?u=alice and receive alice’s messages. In a real page the value must
be the authenticated identity (#{request.remoteUser}, the Jakarta Security principal name, or a server-side
user id), never anything the browser can choose. Do not put an entire user entity there: the value must be
Serializable and small. See the implicit objects for
request.
The OmniFaces documentation adds a caveat for session-scoped user sockets: if the application lets the logged-in user change without invalidating the session, a reused socket can still address the previous user. Invalidate the session on logout, which is good practice anyway.
Updating the UI from a Push: f:ajax and h:commandScript
Writing DOM code in onmessage is fine for a list of lines but awkward when the new data should flow through the
component tree, with converters, formatting and templates. Two documented patterns let the push trigger an ordinary
Faces Ajax request that re-renders components.
Nested f:ajax. UIWebsocket is a client behavior holder that accepts any event name, so the message string is the
event name. Send the string, and the f:ajax whose event equals it fires:
<h:panelGroup id="count" layout="block">
<h:outputText value="Reviews so far: #{liveCount.total}"/>
</h:panelGroup>
<h:form>
<f:websocket channel="reviewCount" scope="view">
<f:ajax event="reviewAdded" listener="#{liveCount.refresh}" render=":count"/>
</f:websocket>
</h:form>
@Named
@RequestScoped
public class LiveCount {
@Inject
private ReviewService service;
@Inject
@Push(channel = "reviewCount")
private PushContext channel;
public int getTotal() {
return service.findAll().size();
}
public void refresh(AjaxBehaviorEvent event) {
// runs as a normal Faces Ajax request; nothing to do, the render attribute re-reads getTotal()
}
public void announce() {
channel.send("reviewAdded"); // called from an action in the same view
}
}
Declare the listener method with an AjaxBehaviorEvent parameter. With a no-argument method Mojarra worked but MyFaces
4.1.4 on Tomcat failed in Invoke Application with MethodNotFoundException: Method not found: ...ping(jakarta.faces.event.AjaxBehaviorEvent).
Both nested-f:ajax runs updated the page when the method took the event.
h:commandScript. h:commandScript (Faces 2.3) generates a JavaScript function that fires a Faces Ajax request with
parameters. Use its name as the onmessage listener. When the message is a Map or a JavaBean, its entries are
posted as request parameters:
<h:form id="g">
<h:commandScript name="refreshReviews" action="#{liveScript.refresh}" render=":list"/>
</h:form>
<h:panelGroup id="list" layout="block">...</h:panelGroup>
<f:websocket channel="reviewScript" scope="view" onmessage="refreshReviews"/>
The h:commandScript form is declared before the tag so the function exists when the tag’s script runs (The Client Side).
With the message Map.of("isbn", "978-0", "count", 3) the action’s request contained isbn=978-0 and count=3 in
getRequestParameterMap() on both implementations, next to the usual jakarta.faces.partial.* parameters.
Both patterns run the full Faces lifecycle for the Ajax request (partial lifecycle),
including restore view and, with execute, validation. They are heavier than a DOM update and fire once per message:
for a chatty channel send one summary message instead of fifty, or coalesce on the server.
Controlling the Connection: connected, rendered and faces.push
By default the socket opens when the page loads. Three ways to change that:
-
Do not connect until needed.
connected="false"renders the script but does not open the socket. Open it from JavaScript withfaces.push.open(clientId)and close it withfaces.push.close(clientId), whereclientIdis the component’s client id (give the tag anid). The functions throw if the client id is unknown. -
Let the server decide.
connected="#{bean.live}"(orrendered) is re-evaluated on every Ajax request by aPreRenderViewEventlistener; when it turnsfalse, the socket is closed in theoncompleteof that request. Tie the expression to a view-scoped property. -
Close after a one-time result. Call
faces.push.close(clientId)inonmessageonce the single expected message arrived.
<h:commandButton value="Start import" onclick="faces.push.open('progress')">
<f:ajax listener="#{importer.start}"/>
</h:commandButton>
<f:websocket id="progress" channel="importProgress" scope="view" connected="false"
onmessage="showProgress"/>
Observed:
-
connected="false", thenfaces.push.open('live')and laterfaces.push.close('live')from the browser console:onopenand, after the close,onclosewith code 1000 ran on Mojarra and MyFaces. -
connected="#{liveToggle.live}with a view-scoped bean flipped by an Ajax button: on Mojarra the socket closed (onclosecode 1000), pushes stopped, and flipping back reopened it. On MyFaces 4.1.4 the socket stayed open and kept receiving afterlivebecamefalse, with or without the tag in therenderlist. If the same code must run on MyFaces, drive the connection from JavaScript withfaces.push.open/close.
Server Events: WebsocketEvent
Faces fires CDI events when a socket opens or closes: jakarta.faces.event.WebsocketEvent with the qualifiers
@WebsocketEvent.Opened and @WebsocketEvent.Closed. The observer must be in an application-scoped bean
(there is no request around those events). The event offers getChannel(), getUser() (generic, cast to your user
type; a ClassCastException for the wrong type) and getCloseCode(), which is null for an opened event:
import jakarta.faces.event.WebsocketEvent;
import jakarta.faces.event.WebsocketEvent.Closed;
import jakarta.faces.event.WebsocketEvent.Opened;
@ApplicationScoped
public class FeedObserver {
private static final Logger log = Logger.getLogger(FeedObserver.class.getName());
public void onOpen(@Observes @Opened WebsocketEvent event) {
log.info("socket opened on " + event.getChannel() + " for " + event.getUser());
}
public void onClose(@Observes @Closed WebsocketEvent event) {
log.info("socket closed on " + event.getChannel() + ", code " + event.getCloseCode());
}
}
With an observer that printed the three values, both implementations reported channel=alerts user=null code=null
when a client connected and code=NORMAL_CLOSURE when it closed the socket normally. A single person can have several sockets on one channel (tabs), so a
"users online" counter must count distinct users, not open events. Use these events for presence and
statistics, never as a security check.
Security
1. Protect the push URL as well as the page. The handshake URL is /jakarta.faces.push/<channel> (the constant
PushContext.URI_PREFIX). A security-constraint on /secure/* protects the page that contains the tag, not the
socket. Add the channel URL to the protected patterns:
<security-constraint>
<web-resource-collection>
<web-resource-name>Members only</web-resource-name>
<url-pattern>/secure/*</url-pattern>
<url-pattern>/jakarta.faces.push/secureFeed</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>USER</role-name>
</auth-constraint>
</security-constraint>
In the scratch run (a page under /secure/ with the channel secureFeed, BASIC authentication) a handshake with the session cookie
but without credentials was refused on both implementations (the client saw close code 1006 and never an onopen), and
with credentials it opened. When the url-pattern line for the channel was removed, the same cookie-only handshake
opened successfully. So the extra pattern is what enforces the role.
2. The registered-channel check. The documentation adds that f:websocket records every channel it rendered in
the current HTTP session and that an incoming handshake for an unknown channel id (guessed, spoofed, or reused after the session expired)
is closed with 1008 (policy violated). Observed with a view-scoped channel: a different session’s cookie,
or no cookie, could not connect to the owner’s channel (Mojarra refused the handshake, client code 1006; MyFaces
opened and closed with 1011); a random id on an application-scoped channel was refused by Mojarra, and MyFaces closed it
with 1011. Application-scoped channels have no owner: on Mojarra a handshake without any cookie succeeded for a valid
application-scope id. Never send private data on an application-scoped channel.
3. Do not trust the browser. Use the authenticated identity for user (see above). The socket carries no commands, so
there is nothing to authorise on the server beyond who may subscribe. Anything the browser wants to do goes through
Faces Ajax, which keeps the view state, the session and your method-level security.
4. Escape on the client. Push messages often carry text from other users. Build DOM nodes with textContent; never
assign pushed strings to innerHTML, and do not interpolate them into eval, Function or URL schemes.
5. Use TLS. Serve the page over HTTPS so the socket is encrypted (wss://), and keep the session cookie Secure
and HttpOnly (see the security page). The scratch runs used plain HTTP.
6. Limit what is sent. The message is the JSON of the object’s getters. A DTO that holds only what every subscriber may see avoids leaking fields by accident.
7. Bound the cost. Every open socket is a server resource. MyFaces reads the parameters
org.apache.myfaces.WEBSOCKET_MAX_IDLE_TIMEOUT (default 300000 ms, as logged at startup) and
org.apache.myfaces.WEBSOCKET_MAX_CONNECTIONS, and org.apache.myfaces.RANDOM_KEY_IN_WEBSOCKET_SESSION_TOKEN (default
secureRandom). Their meaning beyond the names and the logged default was not tested. On Mojarra the limits come from the
server’s own WebSocket container settings.
Behind a Proxy and in a Cluster
Application- and user-targeted channels are kept in the memory of the JVM that rendered the page. Two consequences (derived from the design, not run in the scratch app):
-
A
sendon node A reaches only the sockets connected to node A. A cluster needs a way to broadcast the event to every node (a message queue or a cluster-wide CDI event) and have each node callPushContext.sendlocally. -
The reverse proxy in front of the servers must forward WebSocket upgrade requests and keep idle connections open longer than the proxy’s default timeout. OmniFaces recommends enabling session persistence so a socket can reconnect after a restart or on another node; the Faces documentation only promises automatic reconnects at growing intervals.
The deployment and clustering page covers sticky sessions and session replication in prose.
OmniFaces o:socket
OmniFaces had the same feature before Faces did: o:socket (available since OmniFaces 2.3) is a UI component with the
same attributes (channel, scope, user, onopen, onmessage, onerror, onclose, connected,
rendered) and a very similar documentation. It is present in OmniFaces 5.5.5, the version used for the comparison on this page (the showcase page is current and no deprecation was found). The differences to know:
| Topic | f:websocket (Faces 4.1) |
o:socket (OmniFaces 5) |
|---|---|---|
Sender API |
|
|
Enabling |
|
Automatic since OmniFaces 5.2 when |
Handshake path |
|
|
Open and close from JavaScript |
|
|
Port |
Context parameter |
Attribute |
Server events |
|
|
Limits |
Implementation specific |
|
<o:socket channel="reviews" onmessage="showReview"/>
import org.omnifaces.cdi.Push;
import org.omnifaces.cdi.PushContext;
@Inject
@Push(channel = "reviews")
private PushContext reviews;
Source: OmniFaces Showcase: o:socket
Choose f:websocket for portability and no extra dependency; choose o:socket if the application already uses
OmniFaces and wants its extra events or limits. Do not mix the two annotations in one class: they are different
types. The OmniFaces page covers installing OmniFaces and its push support; the same showcase also has o:sse (Server-Sent
Events) for one-way text push over plain HTTP, which Faces itself does not provide (a proposal exists in the Faces
issue tracker, #1587, without a milestone on 2026-10-08).
PrimeFaces: p:push Is Gone, Use f:websocket
PrimeFaces Push (the push feature of PrimeFaces 4 to 6) was removed in PrimeFaces 7.0; the 7.0 migration guide says to use the JSF 2.3
socket or OmniFaces instead. A PrimeFaces 16 application therefore pushes with f:websocket or o:socket, and
updates components from the listener with a PrimeFaces p:remoteCommand (described on the PrimeFaces Ajax page) or the standard h:commandScript described
above. When push is overkill, PrimeFaces still has p:poll, an Ajax component that sends periodic requests
(<p:poll listener="#{counterBean.increment}" update="txt_count"/> inside a form). Polling costs a request per interval
per client but needs no WebSocket support in the server and passes every proxy; it is a reasonable fallback for a
dashboard that refreshes every 30 seconds.
Testing and Debugging a Push
-
Browser tools. In the Network tab filter by
WS: it shows the handshake URL and each frame. A frame on Mojarra looks like{"data":{"reviewer":"Ana",...}}, on MyFaces like{"reviewer":"Ana",...}. -
Is it a naming problem? If nothing arrives, compare the channel in the tag with the injection point’s field name (The Server Side: @Push and PushContext) and check the endpoint parameter (Enabling the Endpoint).
-
A scripted client. Node.js has a global
WebSocket(22 and later); theheadersoption used below was run on Node.js 24. This script reads the page, extracts the socket URL from thefaces.push.initcall, connects with the page’s session cookie and prints the frames. It is how most of the observations on this page were made:
const base = "http://localhost:8080/bookshelf";
const response = await fetch(`${base}/live.xhtml`);
const cookie = response.headers.getSetCookie().map(c => c.split(";")[0]).join("; ");
const html = await response.text();
const [, , path] = html.match(/faces\.push\.init\('([^']*)','([^']*)'/);
const url = path.startsWith("/") ? `ws://localhost:8080${path}` : path; // MyFaces renders a relative URL
const socket = new WebSocket(url, { headers: { Cookie: cookie } });
socket.onopen = () => console.log("open");
socket.onmessage = event => console.log("message", event.data);
socket.onclose = event => console.log("close", event.code);
Source: MDN: WebSocket and RFC 6455: The WebSocket Protocol
In an automated test, drive the page with Selenium or Playwright (see the end-to-end testing page of this repository, E2E testing in real browsers) and wait for the DOM change the push causes instead of sleeping. The Faces testing page is the place for Arquillian and unit-level strategies.
Mojarra versus MyFaces
Everything observed on Tomcat 11.0.26 and Weld 6.0.4 is collected here (the details are in the sections above):
| Behaviour | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
Wire format |
|
Bare JSON |
Endpoint parameter missing |
Clear error at render time |
Renders, but the socket cannot connect |
Invalid channel name, scope or channel reuse across scopes |
Validated, descriptive errors |
Mostly accepted |
Listener defined after the tag |
|
Works |
Records, enums, |
|
Invalid JSON (record) or nested objects |
|
Bound to the first view or session |
Worked for both users |
Server-driven |
Closes and reopens the socket |
No effect observed |
|
Works |
|
Unauthorised handshake |
Refused (client sees 1006) |
Opened then closed (1011) |
Relative or absolute socket URL |
Absolute |
Relative |
Jakarta JSON Processing missing from the WAR (Tomcat 11.0.26) |
|
Pushed without it; frame is a bare JSON string |
What Changed by Version
-
JSF 2.3 (Java EE 8, 2017):
f:websocket,@Push,PushContext,WebsocketEvent,UIWebsocketand thejsf.pushJavaScript functions are introduced ("WebSocket Integration", issue 1396 in the specification’s changelog), together withh:commandScript. The context parameters arejavax.faces.ENABLE_WEBSOCKET_ENDPOINTandjavax.faces.WEBSOCKET_ENDPOINT_PORT. -
Faces 3.0 (Jakarta EE 9): package and parameter prefix change from
javax.facestojakarta.faces; the tag namespace stays the legacy one. Jakarta WebSocket 2.0 replacesjavax.websocket. -
Faces 4.0 (Jakarta EE 10): the
onerrorattribute is added (issue 1573, milestone 4.0 in the Faces tracker);jsf.pushbecomesfaces.push; the tag namespace isjakarta.faces.core. -
Faces 4.1 (Jakarta EE 11): the 4.1 change log lists nothing about
f:websocket,@PushorPushContext; the optional dependency is Jakarta WebSocket 2.2. -
Faces 5.0 (upcoming, Jakarta EE 12; latest milestone 5.0.0-M9 on 2026-09-18): a search of the Faces issue tracker on 2026-10-08 found no 5.0 change specific to
f:websocket. Open issues on 2026-10-08: a JavaScript API to register WebSocket event listeners (#1442, milestone 5.1) and a Server-Sent Events alternative (#1587, no milestone). Do not rely on either.
The Jakarta EE Tutorial page for WebSockets is marked "last updated for Jakarta EE Version 9.1": it does not mention
onerror, which the 4.0 VDL lists.
Where the Books Are Outdated
-
Ed Burns and Chris Schalk’s JavaServer Faces 2.0: The Complete Reference (2010) and the other JSF 2.0 and earlier books predate JSF 2.3: they have no server push in the framework. Their answers to "live" pages are polling with Ajax, hand-written
XMLHttpRequestcode and third-party libraries. The current answer isf:websocketfor push andf:ajaxorh:commandScriptfor the request back. -
Pro JSF and Ajax (2006) and JSF 2.0 Cookbook (2010) describe Ajax4jsf, Dynamic Faces, Weblets and RichFaces push-style components; all of those are gone (see the Ajax page for the standard replacement).
-
PrimeFaces Cookbook, Second Edition (2015) and Learning PrimeFaces Extensions Development (2014) were written for PrimeFaces 4 and 5, where push meant PrimeFaces Push. It was removed in PrimeFaces 7.0; use
f:websocketoro:socket. -
Everything those books show uses the
javax.facespackages and, where a WebSocket server is mentioned at all, the non-standard APIs of the day. The current packages arejakarta.faces.push,jakarta.faces.component.UIWebsocketandjakarta.faces.event.WebsocketEvent.
Pitfalls
-
Forgot the context parameter. Mojarra tells you; MyFaces renders a socket that never opens.
-
The channel name is the field name.
@Inject @Push PushContext reviewsChannelsends toreviewsChannel. -
EL in
channelorscope. Not allowed. Use fixed names; one global channel with a typed message is better than many per-view channels. -
Listeners declared after the tag. A
ReferenceErroron Mojarra. -
Sending records, enums or
java.timevalues. Silent{}, invalid JSON, or an exception, depending on the implementation. Send strings, numbers and simple beans. -
Session or view scope from an application-scoped bean. Wrong audience on Mojarra. Send from a bean in the same request, or use
user. -
userfrom a request parameter. Anyone can subscribe to anyone else’s messages. -
Private data on an application channel. Everyone can connect.
-
Unprotected push URL. Protect
/jakarta.faces.push/<channel>as well as the page. -
innerHTMLwith pushed text. Cross-site scripting. -
Sending in the same instant as
onopen. Registration finishes slightly later; load the current state in the page. -
Treating push as delivery. Missed messages are not replayed.
-
Tomcat without JSON-P.
NoClassDefFoundError: jakarta/json/Jsonon the first send with Mojarra.
References
-
Jakarta EE Tutorial: Using WebSockets with Jakarta Faces Technology
-
Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters
-
Jakarta Faces 4.1 specification, B.4 Changes between 2.2 and 2.3
-
Jakarta Faces 4.1 specification, B.2 Changes between 4.0 and 3.0
-
Jakarta CDI 4.1 specification: Transactional observer methods
-
Jakarta Faces issue 1442: JavaScript API for WebSocket event listeners
-
Burns and Schalk, JavaServer Faces 2.0: The Complete Reference (publisher page)