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

  1. Is open-xchange-jetty installed? In containerized deployments both engines are part of the core image — nothing to do.
  2. Do you deploy Grizzly-typed WebSocket applications? These are bundles that register WebSocket applications through the WebApplicationService API (in practice: OMF target, Documents RT2). If yes, you need the WebSocket side-car and one routing change. Applications using standard jakarta.websocket need nothing. If you have no WebSocket applications at all, skip the side-car entirely.
  3. 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 (e.g. in /opt/open-xchange/etc/):

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 add a path rule to the ingress (the ingress is not part of the core-mw chart):

# Ingress: one rule per WebSocket context path
- path: /<websocket-context-path>
  pathType: Prefix
  backend:
    service:
      name: middleware-http-api
      port:
        number: 8011

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:

  1. 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
  1. GET :8009/ready returns 200, GET :8016/live returns 200 — probes unchanged.
  2. Login and API calls work through the regular endpoint.
  3. 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 named com.openexchange.http.grizzly.* pendant applies. An explicitly set Jetty key always wins. 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 (default true).
  • 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.
hasAccessLogEnabled, accesslog.statusThreshold, accesslog.synchronous, accesslog.timezone On Jetty the access log is enabled by the presence of accesslog.file; these options have no pendant. 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

  • BindException in the log: the side-car port collides with another listener. Verify it differs from com.openexchange.connector.networkListenerPort, ...networkSslListenerPort and ...livenessPort.
  • Side-car does not start although the port is set: com.openexchange.http.grizzly.hasWebSocketsEnabled is set to false — 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 ingress/ProxyPass rule 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.