jetty.project icon indicating copy to clipboard operation
jetty.project copied to clipboard

NPE on trying to read uri, headers or attributes from the original HttpServletRequest wrapped in UpgradeRequest on WebSocket server in Jetty 12

Open samfrown opened this issue 1 year ago • 4 comments

Jetty version(s) Jetty 12.0.5

Jetty Environment jetty-core, jetty-ee10

Java version/vendor (use: java -version) openjdk 21 2023-09-19 LTS OpenJDK Runtime Environment Corretto-21.0.0.35.1 (build 21+35-LTS) OpenJDK 64-Bit Server VM Corretto-21.0.0.35.1 (build 21+35-LTS, mixed mode, sharing)

OS type/version Linux and MacOS

Description I've migrated an application service from jetty 11.x to 12.x and faced an issue with WebSocket. The server-side part on WebSocketOpen gets some headers and attributes from the HttpServletRequest wrapped into UpgradeRequest, like

public void onWebSocketOpen(Session sess)
    {
        this.session = sess;
        if (sess.getUpgradeRequest() instanceof JettyServerUpgradeRequest) {
            String path = ((JettyServerUpgradeRequest) sess.getUpgradeRequest()).getHttpServletRequest().getRequestURI();
            LOG.debug("Endpoint open: {}, path: {}", sess, path);
            var attrs1 = ((JettyServerUpgradeRequest) sess.getUpgradeRequest()).getServletAttributes().keySet();
            LOG.debug("Attrs1: {}", attrs1);
            var attrs2 = ((JettyServerUpgradeRequest) sess.getUpgradeRequest()).getHttpServletRequest().getAttributeNames();
            LOG.debug("Attrs2: {}", Collections.list(attrs2));
        } else {
            LOG.debug("Endpoint open: {}", sess);
        }
    }

The getHttpServletRequest() returns an instance of servlet request here but throws an NPE on either getServletAttributes() or getHttpServletRequest().getAttribute(...) or getHttpServletRequest().getRequestUri(), because "org.eclipse.jetty.ee10.servlet.ServletApiRequest.getRequest()" is null like:

org.eclipse.jetty.websocket.api.exceptions.CloseException: org.eclipse.jetty.websocket.core.exception.WebSocketException: EventEndpoint OPEN method error: Cannot invoke "org.eclipse.jetty.server.Request.getHttpURI()" because the return value of "org.eclipse.jetty.ee10.servlet.ServletApiRequest.getRequest()" is null
	at org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler.convertCause(JettyWebSocketFrameHandler.java:425)
	at org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler.onError(JettyWebSocketFrameHandler.java:239)
	at org.eclipse.jetty.websocket.core.WebSocketCoreSession.lambda$closeConnection$2(WebSocketCoreSession.java:271)
	at org.eclipse.jetty.server.handler.ContextHandler$ScopedContext.run(ContextHandler.java:1202)
	at org.eclipse.jetty.server.handler.ContextHandler$ScopedContext.run(ContextHandler.java:1195)
	at org.eclipse.jetty.websocket.core.server.internal.AbstractHandshaker$1.handle(AbstractHandshaker.java:179)
	at org.eclipse.jetty.websocket.core.WebSocketCoreSession.closeConnection(WebSocketCoreSession.java:271)
	at org.eclipse.jetty.websocket.core.WebSocketCoreSession.lambda$sendFrame$7(WebSocketCoreSession.java:492)
	at org.eclipse.jetty.util.Callback$3.succeeded(Callback.java:162)
	at org.eclipse.jetty.websocket.core.util.TransformingFlusher.notifyCallbackSuccess(TransformingFlusher.java:195)
	at org.eclipse.jetty.websocket.core.util.TransformingFlusher$Flusher.process(TransformingFlusher.java:152)
	at org.eclipse.jetty.util.IteratingCallback.processing(IteratingCallback.java:250)
	at org.eclipse.jetty.util.IteratingCallback.iterate(IteratingCallback.java:231)
	at org.eclipse.jetty.websocket.core.util.TransformingFlusher.sendFrame(TransformingFlusher.java:78)
	at org.eclipse.jetty.websocket.core.WebSocketCoreSession.sendFrame(WebSocketCoreSession.java:495)
	at org.eclipse.jetty.websocket.core.WebSocketCoreSession.close(WebSocketCoreSession.java:223)
	at org.eclipse.jetty.websocket.core.WebSocketCoreSession.processHandlerError(WebSocketCoreSession.java:354)
	at org.eclipse.jetty.websocket.core.WebSocketCoreSession.lambda$onOpen$5(WebSocketCoreSession.java:385)
	at org.eclipse.jetty.util.Callback$3.failed(Callback.java:168)
	at org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler.onOpen(JettyWebSocketFrameHandler.java:151)
	at org.eclipse.jetty.websocket.core.WebSocketCoreSession.lambda$onOpen$6(WebSocketCoreSession.java:391)
	at org.eclipse.jetty.server.handler.ContextHandler$ScopedContext.run(ContextHandler.java:1208)
	at org.eclipse.jetty.server.handler.ContextHandler$ScopedContext.run(ContextHandler.java:1195)
	at org.eclipse.jetty.websocket.core.server.internal.AbstractHandshaker$1.handle(AbstractHandshaker.java:179)
	at org.eclipse.jetty.websocket.core.WebSocketCoreSession.onOpen(WebSocketCoreSession.java:391)
	at org.eclipse.jetty.websocket.core.WebSocketConnection.onOpen(WebSocketConnection.java:544)
	at org.eclipse.jetty.io.AbstractEndPoint.upgrade(AbstractEndPoint.java:435)
	at org.eclipse.jetty.server.internal.HttpConnection$HttpStreamOverHTTP1.succeeded(HttpConnection.java:1513)
	at org.eclipse.jetty.server.HttpStream$Wrapper.succeeded(HttpStream.java:221)
	at org.eclipse.jetty.server.internal.CompletionStreamWrapper.succeeded(CompletionStreamWrapper.java:45)
	at org.eclipse.jetty.server.HttpStream$Wrapper.succeeded(HttpStream.java:221)
	at org.eclipse.jetty.session.AbstractSessionManager$SessionStreamWrapper.succeeded(AbstractSessionManager.java:1465)
	at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.completeStream(HttpChannelState.java:699)
	at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:629)
	at org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:424)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
	at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:971)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1201)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1156)
	at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: org.eclipse.jetty.websocket.core.exception.CloseException: org.eclipse.jetty.websocket.core.exception.WebSocketException: EventEndpoint OPEN method error: Cannot invoke "org.eclipse.jetty.server.Request.getHttpURI()" because the return value of "org.eclipse.jetty.ee10.servlet.ServletApiRequest.getRequest()" is null
	... 25 more
Caused by: org.eclipse.jetty.websocket.core.exception.WebSocketException: EventEndpoint OPEN method error: Cannot invoke "org.eclipse.jetty.server.Request.getHttpURI()" because the return value of "org.eclipse.jetty.ee10.servlet.ServletApiRequest.getRequest()" is null
	... 23 more
Caused by: java.lang.NullPointerException: Cannot invoke "org.eclipse.jetty.server.Request.getHttpURI()" because the return value of "org.eclipse.jetty.ee10.servlet.ServletApiRequest.getRequest()" is null
	at org.eclipse.jetty.ee10.servlet.ServletApiRequest.getRequestURI(ServletApiRequest.java:371)
	at org.eclipse.jetty.demo.EventEndpoint.onWebSocketOpen(EventEndpoint.java:43)
	at org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler.onOpen(JettyWebSocketFrameHandler.java:142)
	... 22 more

How to reproduce?

It can be reproduced with the sample server from: https://github.com/jetty-project/embedded-jetty-websocket-examples/blob/638ed97c30a81345dc160d1348191eb50d0d6875/native-jetty-websocket-example/src/main/java/org/eclipse/jetty/demo/EventEndpoint.java#L36 with just adding sample code from the 'Description' above.

samfrown avatar Jan 19 '24 19:01 samfrown

Once you reach @OnOpen the request is no longer sane and is quite likely already been recycled / invalidated / reset / etc... It was never sane to read it even in Jetty 9 thru Jetty 11 too.

Why? well, you are past the HTTP layer and are now upgraded entirely to WebSocket. Technically speaking, from a spec, and protocol point of view, the HTTP request / response is complete once the upgrade starts.

The Request object should only be read from steps during the WebSocket handshake.

For the Jetty API, that's one of the WebSocketCreator types. For the Jakarta WebSocket API, that's the ServletEndpointConfig.Configurator.

joakime avatar Jan 19 '24 21:01 joakime

@lachlan-roberts we should have a state change once past upgrade to make the various now invalid methods on Request / HttpServletRequest / etc that can be used to throw a meaningful exception. Something along the lines of IllegalStateException("HTTP Request already completed and is no longer available post WebSocket Upgrade")

joakime avatar Jan 19 '24 21:01 joakime

@lachlan-roberts the methods on UpgradeRequest could return immutable values, not relying on HttpServletRequest existing. But that doesn't help the OP with HttpServletRequest.getAttribute() calls.

joakime avatar Jan 20 '24 02:01 joakime

Thanks a lot for the answer. The explanation looks reasonable. I will proceed with adapting WebSocketCreator with Jetty 12.

samfrown avatar Jan 20 '24 11:01 samfrown