Switching the HTTP Engine (Grizzly to Jetty) deprecated
Since 8.52 the middleware ships two HTTP engines: Grizzly (the default) and Jetty (opt-in). The switch is a single property; ports, probes and the load balancer stay untouched, and rollback is just flipping the property back. This runbook walks through the switch — including the optional Grizzly WebSocket side-car for deployments that still run Grizzly-typed WebSocket applications.
TL;DR
com.openexchange.http.jetty.enabled=true
# only if you deploy Grizzly-typed WebSocket applications (e.g. OMF target, Documents RT2):
com.openexchange.http.grizzly.websocketSidecarPort=8011
Restart the middleware. Done — everything else below is preparation, verification and detail.
Step 1 — Check three things before you switch
- Is
open-xchange-jettyinstalled? In containerized deployments both engines are part of the core image — nothing to do. On a plain installation, install it alongsideopen-xchange-grizzlyand keep both: the package is inert until the switch below is set, and only a still-installed Grizzly makes the rollback a configuration change rather than a package operation. - Do you deploy Grizzly-typed WebSocket applications? These are bundles that register WebSocket applications through the
WebApplicationServiceAPI (in practice: OMF target, Documents RT2). If yes, you need the WebSocket side-car and one routing change. Applications using standardjakarta.websocketneed nothing. If you have no WebSocket applications at all, skip the side-car entirely. - Do you override one of the few Grizzly-only properties? Almost all tuning carries over automatically (see How configuration carries over). Only the properties in the Grizzly-only table are ignored by Jetty — check the table if you override any of them.
Do the first switch on a staging system and run your usual client checks against it.
Step 2 — Set the properties
Plain installation — the engine ships no configuration file of its own, so create /opt/open-xchange/etc/jetty.properties (the file the property documentation names):
com.openexchange.http.jetty.enabled=true
# only for the side-car:
com.openexchange.http.grizzly.websocketSidecarPort=8011
Kubernetes (core-mw chart):
core-mw:
properties:
com.openexchange.http.jetty.enabled: "true"
# only for the side-car:
com.openexchange.http.grizzly.websocketSidecarPort: "8011"
Pick a free port for the side-car — note that 8010 is already the default HTTPS connector port (com.openexchange.connector.networkSslListenerPort).
Step 3 — Route WebSocket paths (side-car only)
Skip this step if you do not use the side-car.
Clients are not affected: they keep connecting through the load balancer with unchanged URLs (wss://host/<path>). Only the routing behind the proxy changes — forward the WebSocket context paths of the affected applications to the side-car port. Everything else keeps going to the HTTP port (8009).
Kubernetes — expose the side-car port via the core-mw chart values. Both containerPorts and the per-role services[].ports are lists, and Helm replaces overridden lists wholesale instead of merging them — always repeat the existing HTTP entries, otherwise probes and the regular HTTP service break:
core-mw:
containerPorts:
- containerPort: 8009 # repeat the default — the list is replaced, not merged!
name: http
- containerPort: 8011
name: ws-sidecar
roles:
http-api: # the role whose service should expose the side-car
services:
- type: ClusterIP
ports:
- port: 80 # repeat the existing entry — same reason
targetPort: http
protocol: TCP
name: http
- port: 8011
targetPort: ws-sidecar
protocol: TCP
name: ws-sidecar
Then route the WebSocket paths to that port. The routing layer is not part of the core-mw chart — with Istio, add one route per WebSocket context path to the VirtualService:
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: <name>
namespace: <namespace>
spec:
gateways:
- <gateway>
hosts:
- <public-host>
http:
- name: rt2-route
match:
- uri:
prefix: /appsuite/rt2 # path as seen by the client
rewrite:
uri: /rt2 # context path the application registered
route:
- destination:
host: <service>.<namespace>.svc.cluster.local
port:
number: 8011
Two things to watch: Istio evaluates http routes in order, so this route must come before the catch-all route that sends /appsuite to the HTTP port. And rewrite is only needed when the public prefix differs from the context path the application registered.
Apache — one line per WebSocket context path (mod_proxy_wstunnel):
ProxyPass /<websocket-context-path> ws://localhost:8011/<websocket-context-path>
Step 4 — Restart and verify
Restart the middleware (in Kubernetes the property change triggers a rolling restart), then check:
- Log lines confirming the engines:
Starting Jetty server.
Started Jetty HTTP network listener on host: 0.0.0.0 and port: 8009
Without side-car additionally: Jetty HTTP engine is enabled. Skipping start-up of Grizzly server.
With side-car instead:
Jetty HTTP engine is enabled. Starting Grizzly in WebSocket side-car mode.
Started Grizzly WebSocket side-car listener on host: 0.0.0.0 and port: 8011
GET :8009/readyreturns200,GET :8016/livereturns200— probes unchanged.- Login and API calls work through the regular endpoint.
- With side-car: a WebSocket client of the affected application connects through the proxy with its unchanged URL.
Rollback
Set com.openexchange.http.jetty.enabled=false (or remove the property) and restart. Grizzly serves everything again, including WebSockets on the HTTP port. Side-car routing rules can stay (they are inert while the side-car port is closed) or be reverted along with it. Nothing else to undo.
The WebSocket side-car (optional)
On Jetty, WebSockets are provided through the standard jakarta.websocket (JSR 356) container. Applications written against the Grizzly-typed WebApplicationService API do not run on it without a code migration. The side-car bridges that gap: Grizzly starts in a WebSocket-only mode on the configured port — original Grizzly WebSocket stack, so the affected applications run completely unchanged. No servlet container, no Comet, no liveness; 0 (the default) disables it entirely.
Both WebSocket stacks run in parallel. A given WebSocket path is routed to exactly one port: Grizzly-typed applications to the side-car port, jakarta.websocket applications to the regular HTTP port (no extra routing needed). When an application gets migrated to jakarta.websocket, flip its path back to 8009; when the last one is migrated, remove the side-car property and the routing rules.
The side-car is a transitional bridge, not a permanent mode — it keeps the Grizzly stack in the deployment. Plan the migration of the affected applications regardless.
Reference
How configuration carries over
Nothing needs to be re-configured for a typical deployment:
- Engine-neutral namespaces (
com.openexchange.connector.*,com.openexchange.server.*,com.openexchange.servlet.*,com.openexchange.cookie.*) apply to both engines unchanged — ports, host, shutdown, cookies, forwarded headers, body limits. - Grizzly tuning falls through: when a
com.openexchange.http.jetty.*key is not set, its equally namedcom.openexchange.http.grizzly.*pendant applies. An explicitly set Jetty key always wins. This includes the access log master switchhasAccessLogEnabled: the log is written when the switch is on (the default) andaccesslog.filenames a file, so a deployment that switched its Grizzly access log off keeps it off after the switch. See the property documentation for the full list.
Settings that only exist in Jetty
com.openexchange.http.jetty.enabled— the engine switch.com.openexchange.http.jetty.http2.enabled— cleartext HTTP/2 (h2c); Grizzly has no HTTP/2 support at all.com.openexchange.http.jetty.metrics.enabled— engine metrics via Micrometer (defaulttrue).com.openexchange.http.jetty.accesslog.retainDays— retention for rotated access logs.com.openexchange.http.jetty.selectorRunnersCount— deliberately not adopted from the Grizzly pendant; Jetty keeps its auto-sizing default.
Settings that only exist in Grizzly
These are ignored by the Jetty engine — relevant only if you override one of them:
| Property | Note |
|---|---|
hasCometEnabled | No Comet on Jetty; Drive long polling uses the engine-neutral servlet-async listener on both engines. |
writeTimeoutMillis | Jetty has a single idle timeout, fed from readTimeoutMillis, and it covers stalled writes as well — no protection is lost. Both Grizzly timeouts default to 60000, so only a deployment that sets them to different values sees a difference: on Jetty the read value applies to both directions. |
keepAlive, minWriteBufferSize | Transport internals without a Jetty equivalent. |
maxQueryStringSize | Inactive on Grizzly, too — the parameter check it feeds is disabled. On both engines the query string is bounded by maxHttpHeaderSize, which covers the request line and answers 414. |
sessionUnjoinedThreshold, removeNonAuthenticatedSessions | Grizzly-specific session eviction; Jetty sessions expire via inactivity only. |
supportHierachicalLookupOnNotFound | Retry-climb on 404 is not replicated; standard servlet longest-prefix matching applies. |
accesslog.statusThreshold, accesslog.synchronous, accesslog.timezone | No pendant on Jetty. accesslog.format values other than common and combined (e.g. agent, referer, the vhost_* variants) are served as combined. |
websocketSidecarPort | Grizzly-side by design — configures the side-car (only effective while Jetty is enabled). |
com.openexchange.servlet.maxActiveSessions (engine-neutral) is enforced on both engines. The eviction differs in detail: Grizzly evicts via its session cache, Jetty invalidates the least recently used among the oldest sessions once the cap is exceeded.
Troubleshooting
BindExceptionin the log: the side-car port collides with another listener. Verify it differs fromcom.openexchange.connector.networkListenerPort,...networkSslListenerPortand...livenessPort.- Side-car does not start although the port is set:
com.openexchange.http.grizzly.hasWebSocketsEnabledis set tofalse— the start-up is skipped with a warning. - WebSocket clients get HTTP 404 after the switch: the application's context path is still routed to the HTTP port. Check the
VirtualServiceroute (order matters!) or theProxyPassrule for the side-car port. - A Grizzly tuning seems lost: check the Grizzly-only table; everything else is adopted via the pendant fallback unless an explicit
com.openexchange.http.jetty.*override shadows it.