Ignition 8.1 Jetty 12 Upgrade

Tomorrow's (4-24-2025) 8.1.49-SNAPSHOT early access build includes a major upgrade of Jetty - the third party library Ignition depends on for powering the Gateway's embedded web server.

The primary reason for the major upgrade is: Jetty v10 is end of life and will no longer receive critical bug fixes and security patches. This is at odds with our commitment for long term support of Ignition 8.1.

Performing a major upgrade of a core dependency such as Jetty is a major undertaking and not without risk. However, our development and QA team has already performed the upgrade early on in our 8.3 branch successfully, and we have learned many lessons from that effort which have been applied successfully in 8.1. We have also undergone a rigorous QA testing cycle in 8.1 to root out any issues early on before we were confident with the change.

Ideally, on upgrade to 8.1.49, you should notice no difference with this upgrade. Module authors should also notice no Ignition API changes, unless you also depend on Jetty APIs, in which case you will want to bump your module's Jetty dependency to v12.0.17.

Because this is such a major upgrade on a minor LTS version upgrade of Ignition, out of an abundance of caution, I am posting this message to encourage folks to try out the 8.1.49 Early Access build for yourselves. If you are a module author, make sure your module is compatible with the upgrade. Please post issues here if you notice anything.

My hope is that with our rigorous internal testing efforts and with an early warning in this forum to give folks plenty of time to raise red flags with this upgrade, by the time the final build of 8.1.49 is published, we can all be confident that Ignition's web server is up-to-date and secure for the LTS years ahead of us.

2 Likes

Will test. If my v8.3 conversions are a guide, I will have to offer v8.1.49+ module files for my Blob Server and my Image Streamer. (Both register servlets, and that requires the v10 APIs that were moved in v12.)

Thanks Phil. I'll note: in 8.1, we bumped the servlet spec from v3.1.0 to v4.0.1, but that should not cause a breaking API change, because we kept on the javax.servlet.* package, whereas in 8.3, we upgraded the servlet spec to v6.0.0 and also moved from the jaxax.servlet.* package to the jakarta.servlet.* package, which was a breaking API change in 8.3. This is important because the servlet API bleeds into the Ignition public API, whereas Jetty APIs are an implementation detail that does not bleed into Ignition's public APIs.

The location of the servlet package didn't trouble me. It was gaining access to the output stream to efficiently deliver big results that led to the moved Jetty API. I haven't figured out Jetty v12's handler architecture yet.

In your case, does the HttpServletResponse's output stream not work?

Thanks for the update, this does impact my Server Sent Events module.

Embr currently targets 8.1.33, but I will eventually change to target whatever becomes 8.1's final(ish) release.

I might just let the SSE module remain broken until then, unless someone complains :man_shrugging: My larger plans for the module entirely revolve around Event Streams, so support of the 8.1 version is not a priority.

1 Like

Ah, it won't be an issue for the Blob Server, after all, because it fires the one payload and done.

I do more in the Image Streamer to detect "closed", which the jetty api offers in the HttpOutputStream.

Hello, in a module with websockets managed by Ignition Jetty werserver, we are using the following artifact:

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>10.0.15</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.eclipse.jetty.websocket</groupId>
            <artifactId>websocket-jetty-api</artifactId>
            <version>10.0.15</version>
            <scope>provided</scope>
        </dependency>
		
        <dependency>
            <groupId>org.eclipse.jetty.websocket</groupId>
            <artifactId>websocket-jetty-server</artifactId>
            <version>10.0.15</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.eclipse.jetty.websocket</groupId>
            <artifactId>websocket-servlet</artifactId>
            <version>10.0.15</version>
            <scope>provided</scope>
        </dependency>

I try to upgrade them for Ignition 8.1.49,

it works for:

        <!-- Core Jetty server -->
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>12.0.17</version>
            <scope>provided</scope>
        </dependency>

but not with:

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>12.0.17</version>
        </dependency>
        
        <dependency>
            <groupId>org.eclipse.jetty.websocket</groupId>
            <artifactId>websocket-jetty-api</artifactId>
            <version>12.0.17</version>
            <scope>provided</scope>
        </dependency>
        
        <dependency>
            <groupId>org.eclipse.jetty.websocket</groupId>
            <artifactId>websocket-jetty-server</artifactId>
            <version>12.0.17</version>
            <scope>provided</scope>
        </dependency>
Could not find artifact org.eclipse.jetty:jetty-servlet:pom:12.0.17 in releases (https://nexus.inductiveautomation.com/repository/inductiveautomation-releases)

I'm not sure I have selected the right new articfact ?

My code with Jetty 10:

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;

import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
import org.eclipse.jetty.websocket.server.JettyWebSocketCreator;

import org.eclipse.jetty.websocket.server.JettyWebSocketServlet;
import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory;

....

    public void createServer(int maxClient) {
        try {
            this.maxClient = maxClient;
            WebResourceManager web = gatewayContext.getWebResourceManager();
            web.addServlet(wsPath, PerspectiveWebSocketServlet.class);
            logger.info("createServer : {} servlet added",wsPath);
            this.executorProcessReception = Executors.newFixedThreadPool(maxClient);
        } catch (Exception e) {
            logger.error("createServer : Exception : ", e);
        }
    }

...

    public static class PerspectiveWebSocketServlet extends JettyWebSocketServlet {

        public PerspectiveWebSocketServlet() {
        }

        @Override
        protected void configure(JettyWebSocketServletFactory jettyWebSocketServletFactory) {
            jettyWebSocketServletFactory.setMaxTextMessageSize(65536);
            jettyWebSocketServletFactory.setCreator(new PerspectiveWebSocketCreator());
        }
    }

    public static class PerspectiveWebSocketCreator implements JettyWebSocketCreator {

        private final Logger logger = LoggerFactory.getLogger(getClass());

        @Override
        public Object createWebSocket(JettyServerUpgradeRequest jettyServerUpgradeRequest, JettyServerUpgradeResponse jettyServerUpgradeResponse) {
            logger.info("createWebSocket");
            return new WebSocketChannel();
        }
    }

    @WebSocket
    public static class WebSocketChannel
    {
        private final Logger logger = LoggerFactory.getLogger(getClass());

        public Session getSession() {
            return memoSession;
        }

        private Session memoSession;

        @OnWebSocketConnect
        public void onWebSocketConnect(Session session)
        {
            try {
                this.memoSession = session;
                String sessionId = String.valueOf(session.hashCode());
                logger.debug("sessionId = {} - onConnect", sessionId);
                sendMessage(session, createSignalingMessage(MESSAGE_READY, ""));
            } catch (Exception e) {
                logger.error("onConnect : Exception : ", e);
            }
        }

        @OnWebSocketMessage
        public void onWebSocketText(String message)
        {
            String sessionId = String.valueOf(getSession().hashCode());
            Session wsSession = getSession();
            try {
                if (!message.equals("HEARTBEAT")) {
                    logger.debug("sessionId = {} - onWebSocketText - LocalAddress = {} - RemoteAddress = {} - message = {}",
                            sessionId,
                            wsSession.getLocalAddress(),
                            wsSession.getRemoteAddress(),
                            message);
                }
                if (message.equals("HEARTBEAT")) {
                    // réponse au heartbeat envoyé par le client JS
                    onWebSocketText_heartbeat(sessionId,message,wsSession);
                } else if (message.startsWith("STOP")) {
...
                }
            } catch (Exception e) {
                logger.error("sessionId = {} - onWebSocketText : Exception : ",sessionId, e);
            }
        }

        @OnWebSocketClose
        public void onWebSocketClose(int statusCode, String reason)
        {
            try {
                String sessionId = String.valueOf(getSession().hashCode());
                logger.debug("sessionId = {} - onWebSocketClose", sessionId);
                stopPipeline(sessionId);
            } catch (Exception e) {
                logger.error("onClose : onWebSocketClose : Exception : ", e);
            }
        }

        @OnWebSocketError
        public void onWebSocketError(Throwable cause)
        {
            //super.onWebSocketError(cause);
        }
    }

@PGriffith help me a lot:

The API is moved to new packages. You have to refactor your code. You cannot fix it just in your build configuration.

According to this, you would now need something like org.eclipse.jetty.ee8:jetty-ee8-servlet.

And you shouldn't need it at all (keep using provided scope), because Ignition already has jetty-ee8-websocket-servlet as a dependency, which has jetty-ee8-servlet as a transitive dependency.

Thanks !
all things seems to be good for 8.1.49 in my usecase with:

        <!-- Core Jetty server -->
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>12.0.17</version>
            <scope>provided</scope>
        </dependency>

        <!-- Jetty servlet support -->
        <dependency>
            <groupId>org.eclipse.jetty.ee8</groupId>
            <artifactId>jetty-ee8-servlet</artifactId>
            <version>12.0.17</version>
            <scope>provided</scope>
        </dependency>

        <!-- Jetty websocket support -->
        <dependency>
            <groupId>org.eclipse.jetty.ee8.websocket</groupId>
            <artifactId>jetty-ee8-websocket-jetty-api</artifactId>
            <version>12.0.17</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.eclipse.jetty.ee8.websocket</groupId>
            <artifactId>jetty-ee8-websocket-jetty-server</artifactId>
            <version>12.0.17</version>
            <scope>provided</scope>
        </dependency>

and

import org.eclipse.jetty.ee8.websocket.api.Session;
import org.eclipse.jetty.ee8.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.ee8.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.ee8.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.ee8.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.ee8.websocket.api.annotations.WebSocket;

import org.eclipse.jetty.ee8.websocket.server.JettyServerUpgradeRequest;
import org.eclipse.jetty.ee8.websocket.server.JettyServerUpgradeResponse;
import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketCreator;

import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServlet;
import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServletFactory;