Accessing HttpSession from HttpServletRequest in a Web Socket @ServerEndpoint
Asked Answered
M

6

50

Is it possible to get the HttpServletRequest inside a @ServerEndpoint? Primarily I am trying to get it so I can access the HttpSession object.

Meshed answered 30/7, 2013 at 0:26 Comment(0)
E
85

Update (November 2016): The information provided in this answer is for the JSR356 spec, individual implementations of the spec may vary outside of this information. Other suggestions found in comments and other answers are all implementation specific behaviors outside of the JSR356 spec.

If the suggestions in here are causing you problems, upgrade your various installations of Jetty, Tomcat, Wildfly, or Glassfish/Tyrus. All current versions of those implementations have all been reported to work in the way outlined below.

Now back to the original answer from August 2013...

The answer from Martin Andersson has a concurrency flaw. The Configurator can be called by multiple threads at the same time, it is likely that you will not have access to the correct HttpSession object between the calls from modifyHandshake() and getEndpointInstance().

Or said another way...

  • Request A
  • Modify Handshake A
  • Request B
  • Modify Handshake B
  • Get Endpoint Instance A <-- this would have Request B's HttpSession
  • Get Endpoint Instance B

Here's a modification to Martin's code that uses ServerEndpointConfig.getUserProperties() map to make the HttpSession available to your socket instance during the @OnOpen method call

GetHttpSessionConfigurator.java

package examples;

import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;

public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator
{
    @Override
    public void modifyHandshake(ServerEndpointConfig config, 
                                HandshakeRequest request, 
                                HandshakeResponse response)
    {
        HttpSession httpSession = (HttpSession)request.getHttpSession();
        config.getUserProperties().put(HttpSession.class.getName(),httpSession);
    }
}

GetHttpSessionSocket.java

package examples;

import java.io.IOException;

import javax.servlet.http.HttpSession;
import javax.websocket.EndpointConfig;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value = "/example", 
                configurator = GetHttpSessionConfigurator.class)
public class GetHttpSessionSocket
{
    private Session wsSession;
    private HttpSession httpSession;
    
    @OnOpen
    public void open(Session session, EndpointConfig config) {
        this.wsSession = session;
        this.httpSession = (HttpSession) config.getUserProperties()
                                           .get(HttpSession.class.getName());
    }
    
    @OnMessage
    public void echo(String msg) throws IOException {
        wsSession.getBasicRemote().sendText(msg);
    }
}

Bonus feature: no instanceof or casting required.

Some EndpointConfig Knowledge

EndpointConfig objects do exist per "Endpoint Instance".

However, an "Endpoint Instance" has 2 meanings with the spec.

  1. Default behavior of the JSR, where each incoming upgrade request results in a new object instance of the endpoint class
  2. A javax.websocket.Session that ties together the object endpoint instance, with its configuration, to a specific logical connection.

It is possible to have a singleton Endpoint instance being used for multiple javax.websocket.Session instances (that is one of the features that ServerEndpointConfig.Configurator supports)

The ServerContainer implementation will track a set of ServerEndpointConfig's that represent all of the deployed endpoints that the server can respond to a websocket upgrade request.

These ServerEndpointConfig object instances can come from a few different sources.

  1. Manually provided by the javax.websocket.server.ServerContainer.addEndpoint(ServerEndpointConfig)
    • Usually done within a javax.servlet.ServletContextInitializer.contextInitialized(ServletContextEvent sce) call
  2. From the javax.websocket.server.ServerApplicationConfig.getEndpointConfigs(Set) call.
  3. Automatically created from scanning of the web application for @ServerEndpoint annotated classes.

These ServerEndpointConfig object instances exist as defaults for when a javax.websocket.Session does eventually get created.

ServerEndpointConfig.Configurator Instance

Before any upgrade requests are received or processed, all of the ServerEndpointConfig.Configurator objects now exist and are ready to perform their main and sole purpose, to allow for customization of the upgrade process of a websocket connection to an eventual javax.websocket.Session

Access to Session specific EndpointConfig

Note, you cannot access the ServerEndpointConfig object instances from within a endpoint instance. You can only access EndpointConfig instances.

This means if you provided ServerContainer.addEndpoint(new MyCustomServerEndpointConfig()) during deploy and later tried to access it via the annotations, it will not work.

All of the following would be invalid.

@OnOpen
public void onOpen(Session session, EndpointConfig config)
{
    MyCustomServerEndpointConfig myconfig = (MyCustomServerEndpointConfig) config;
    /* this would fail as the config is cannot be cast around like that */
}

// --- or ---

@OnOpen
public void onOpen(Session session, ServerEndpointConfig config)
{
    /* For @OnOpen, the websocket implementation would assume
       that the ServerEndpointConfig to be a declared PathParam
     */
}

// --- or ---

@OnOpen
public void onOpen(Session session, MyCustomServerEndpointConfig config)
{
    /* Again, for @OnOpen, the websocket implementation would assume
       that the MyCustomServerEndpointConfig to be a declared PathParam
     */
}

You can access the EndpointConfig during the life of the Endpoint object instance, but under a limited time. The javax.websocket.Endpoint.onOpen(Session,Endpoint), annotated @OnOpen methods, or via the use of CDI. The EndpointConfig is not available in any other way or at any other time.

However, you can always access the UserProperties via the Session.getUserProperties() call, which is available always. This User Properties map is always available, be it via the annotated techniques (such as a Session parameter during @OnOpen, @OnClose, @OnError, or @OnMessage calls), via CDI injection of the Session, or even with the use of non-annotated websockets that extend from javax.websocket.Endpoint.

How Upgrade Works

As stated before, every one of the defined endpoints will have a ServerEndpointConfig associated with it.

Those ServerEndpointConfigs are a single instance that represents the default state of the EndpointConfig that are eventually made available to the Endpoint Instances that are possibly and eventually created.

When a incoming upgrade request arrives, it has go through the following on the JSR.

  1. does the path match any of the ServerEndpointConfig.getPath() entries
    • If no match, return 404 to upgrade
  2. pass upgrade request into ServerEndpointConfig.Configurator.checkOrigin()
    • If not valid, return error to upgrade response
    • create HandshakeResponse
  3. pass upgrade request into ServerEndpointConfig.Configurator.getNegotiatedSubprotocol()
    • store answer in HandshakeResponse
  4. pass upgrade request into ServerEndpointConfig.Configurator.getNegotiatedExtensions()
    • store answer in HandshakeResponse
  5. Create new endpoint specific ServerEndpointConfig object. copy encoders, decoders, and User Properties. This new ServerEndpointConfig wraps default for path, extensions, endpoint class, subprotocols, configurator.
  6. pass upgrade request, response, and new ServerEndpointConfig into ServerEndpointConfig.Configurator.modifyHandshake()
  7. call ServerEndpointConfig.getEndpointClass()
  8. use class on ServerEndpointConfig.Configurator.getEndpointInstance(Class)
  9. create Session, associate endpoint instance and EndpointConfig object.
  10. Inform endpoint instance of connect
  11. annotated methods that want EndpointConfig gets the one associated with this Session.
  12. calls to Session.getUserProperties() returns EndpointConfig.getUserProperties()

To note, the ServerEndpointConfig.Configurator is a singleton, per mapped ServerContainer endpoint.

This is intentional, and desired, to allow implementors several features.

  • to return the same Endpoint instance for multiple peers if they so desire. The so called stateless approach to websocket writing.
  • to have a single point of management of expensive resources for all Endpoint instances

If the implementations created a new Configurator for every handshake, this technique would not be possible.

(Disclosure: I write and maintain the JSR-356 implementation for Jetty 9)

Extreme answered 1/8, 2013 at 12:38 Comment(19)
1. Did you know that the EndpointConfig object passed to your @OnOpen method, if you're using Tyrus, is not your custom configuration object but it is the default? That made me not trust the user properties map. Besides, you don't have to use the instanceof operator. I used it to make sure we get a proper exception type if a future programmer uses the custom class inappropriately. 2. It may look like a concurrency flaw but it isn't. I have emphasized this in my answer already. The JavaDoc says: "The implementation creates a new instance of the configurator per logical endpoint."Trifle
Since Archimedes Trajano accepted your answer and the statement that my answer contains a "concurrency flaw". I would be glad if anyone of you could elaborate a bit why you think that way. Maybe I have misunderstood the specification and/or the JavaDocs of the WebSocket API.Trifle
A quote from page 23 of JSR-356: "[..] the container must use a unique instance of the endpoint per peer". But again, you are two guys against me so I reserve the honor of being wrong. But please let me know why. I tested the user properties map and it seems to work even though the properties map is supposed to be unique per configuration object instance and the objects passed in are different. One being our custom object and the other one being the Tyrus default. I would say that the bonus feature added with such a design is not having to override the getEndpointInstance method at all.Trifle
ping. Joakim (and implicitly Archimedes), you kind of have a community responsibility to update your answer or make sense out of the claim that storing a variable in a custom Configurator object isn't thread safe.Trifle
I implemented this example code on Tomcat 8.0.0-RC3 and request.getHttpSession() returns null. I think it's time to return to AJAX long polling. The fact that Tomcat and Jetty are exhibiting completely different behaviour does not bode well for the near future of the WebSocket standard.Projector
@studro it actually points to a poor spec for javax.websocket, not "the WebSocket standard".Extreme
Great point. Is there an alternative Java WebSocket specification and implementation that you can recommend that will work in both Tomcat and Jetty containers?Projector
@Joakim Looking at the source code of Jetty 9.1.x in JsrCreator.java, it's clear that modifyHandshake receives the original ServerEndpointConfig and not the per-handshake copy. Compared to the flow you describe, modifyHandshake (point 6) happens even before checkOrigin (point 2). It is exactly the same with GlassFish 4, so I don't see how using userProperties() is more threadsafe than using instance variables of the configurator. In fact, I don't see how it's possible to reliably pass anything between the configurator and the endpoint instance with current JSR 356 impls.Juanjuana
ServerEndpointConfig.Configurator can be created by the implementation (via annotations) or be passed in pre-instantiated via other parts of the javax.websocket standard. The safe approach is to instantiate once and reuse, unlike the alternative with per-handshake copies.Extreme
I guess my explanation was not very clear. What I meant is just that modifyHandshake receives a 'singleton' ServerEndpointConfig so it's not threadsafe to use endpointConfig.userProperties to store the session. From what I've checked, none of the two answers to the original question about storing the httpsession is right.Juanjuana
Just to be complete, Tomcat does pass the per-session endpointConfig to modifyHanshake so the answer is correct for Tomcat but not for Jetty and GlassFish. The spec does not seem to specify any particular behaviour.Juanjuana
HttpSession httpSession = (HttpSession)request.getHttpSession(); will return null in my case. I thought a WS connection is initiated within a http-request? So there should be a HttpSession. Do you have any idea whats wrong here? EDIT: Looks like the HttpSession is not created yet. So I just have to create a Session before I allow access to the WebsocketPine
I just deployed an spring mcv application and my websocket project to the same jboss. The Spring mvc fronted will show my httpSession. However, the Webservice still won't read the HttpSession.Pine
Same problem for Wildfly endpointConfig.userProperties is absolutely not thread safe, and for the record I also get null in (HttpSession)request.getHttpSession() but I'm using instead request.getHeaders() that returns the https headers... the problem is I can't pass to the ServerEnpoint because I can't use userPropertiesBushweller
After lot of tests I was unable to get this working using a brand new wildfly with a simple websocket appBushweller
Could you take a look at this question #39741031Ddt
Is there a way to copy websocket session attributes back to Http Session?Centrist
No, as the HttpSession has no lifecycle once you are upgraded past http into websocket. (This is undefined by the Servlet spec and WebSocket spec).Extreme
2022.03.14, unfortunately, when my program running on JBoss EAP 7.4 the EndpointConfig instance is just the one (ServerEndpointConfig) used in configurator, so request A & request B shares the same user data, I also tried ThreadLocal, modifyHandshake is not run in the same thread as onOpen, no solutions so far. :(Reluctivity
C
21

Preface

It's unclear whether you're wanting the HttpServletRequest, the HttpSession, or properties out of the HttpSession. My answer will show how to get the HttpSession or individual properties.

I've omitted null and index bounds checks for brevity.

Cautions

It's tricky. The answer from Martin Andersson is not correct because the same instance of ServerEndpointConfig.Configurator is used for every connection, hence a race condition exists. While the docs state that "The implementation creates a new instance of the configurator per logical endpoint," the spec does not clearly define a "logical endpoint." Based on the context of all the places that phrase is used, it appears to mean the binding of a class, configurator, path, and other options, i.e., a ServerEndpointConfig, which is clearly shared. Anyway you can easily see if an implementation is using the same instance by printing out its toString() from within modifyHandshake(...).

More surprisingly the answer from Joakim Erdfelt also does not work reliably. The text of JSR 356 itself does not mention EndpointConfig.getUserProperties(), it is only in the JavaDoc, and nowhere does it seem to be specified what its exact relationship is to Session.getUserProperties(). In practice some implementations (e.g., Glassfish) return the same Map instance for all calls to ServerEndpointConfig.getUserProperties() while others (e.g., Tomcat 8) don't. You can check by printing out the map contents before modifying it within modifyHandshake(...).

To verify, I copied the code directly from the other answers and then tested it against a multithreaded client I wrote. In both cases I observed the incorrect session being associated with the endpoint instance.

Outline of Solutions

I've developed two solutions, which I've verified work correctly when tested against a multithreaded client. There are two key tricks.

First, use a filter with the same path as the WebSocket. This will give you access to the HttpServletRequest and HttpSession. It also gives you a chance to create a session if it doesn't already exist (although in that case using an HTTP session at all seems dubious).

Second, find some properties that exist in both the WebSocket Session and HttpServletRequest or HttpSession. It turns out there are two candidates: getUserPrincipal() and getRequestParameterMap(). I will show you how to abuse both of them :)

Solution using User Principal

The easiest way is to take advantage of Session.getUserPrincipal() and HttpServletRequest.getUserPrincipal(). The downside is this could interfere with other legitimate uses of this property, so use it only if you are prepared for those implications.

If you want to store just one string, such as a user ID, this is actually not too much of an abuse, though it presumably should be set in some container managed way rather than overriding the wrapper as I'll show you. Anyway you would do that by just overriding Principal.getName(). Then you don't even need to cast it in the Endpoint. But if you can stomach it, you can also pass the whole HttpSession object as follows.

PrincipalWithSession.java

package example1;

import java.security.Principal;
import javax.servlet.http.HttpSession;

public class PrincipalWithSession implements Principal {
    private final HttpSession session;

    public PrincipalWithSession(HttpSession session) {
        this.session = session;
    }

    public HttpSession getSession() {
        return session;
    }

    @Override
    public String getName() {
        return ""; // whatever is appropriate for your app, e.g., user ID
    }
}

WebSocketFilter.java

package example1;

import java.io.IOException;
import java.security.Principal;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

@WebFilter("/example1")
public class WebSocketFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        final PrincipalWithSession p = new PrincipalWithSession(httpRequest.getSession());
        HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(httpRequest) {
            @Override
            public Principal getUserPrincipal() {
                return p;
            }
        };
        chain.doFilter(wrappedRequest, response);
    }

    public void init(FilterConfig config) throws ServletException { }
    public void destroy() { }
}

WebSocketEndpoint.java

package example1;

import javax.servlet.http.HttpSession;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/example1")
public class WebSocketEndpoint {
    private HttpSession httpSession;

    @OnOpen
    public void onOpen(Session webSocketSession) {
        httpSession = ((PrincipalWithSession) webSocketSession.getUserPrincipal()).getSession();
    }

    @OnMessage
    public String demo(String msg) {
        return msg + "; (example 1) session ID " + httpSession.getId();
    }
}

Solution Using Request Parameters

The second option uses Session.getRequestParameterMap() and HttpServletRequest.getParameterMap(). Note that it uses ServerEndpointConfig.getUserProperties() but it is safe in this case because we're always putting the same object into the map, so whether it's shared makes no difference. The unique session identifier is passed not through the user parameters but instead through the request parameters, which is unique per request.

This solution is slightly less hacky because it does not interfere with the user principal property. Note that if you need to pass through the actual request parameters in addition to the one that's inserted, you can easily do so: just start with the existing request parameter map instead of a new empty one as shown here. But take care that the user cannot spoof the special parameter added in the filter by supplying their own request parameter by the same name in the actual HTTP request.

SessionTracker.java

/* A simple, typical, general-purpose servlet session tracker */
package example2;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

@WebListener
public class SessionTracker implements ServletContextListener, HttpSessionListener {
    private final ConcurrentMap<String, HttpSession> sessions = new ConcurrentHashMap<>();

    @Override
    public void contextInitialized(ServletContextEvent event) {
        event.getServletContext().setAttribute(getClass().getName(), this);
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
    }

    @Override
    public void sessionCreated(HttpSessionEvent event) {
        sessions.put(event.getSession().getId(), event.getSession());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        sessions.remove(event.getSession().getId());
    }

    public HttpSession getSessionById(String id) {
        return sessions.get(id);
    }
}

WebSocketFilter.java

package example2;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

@WebFilter("/example2")
public class WebSocketFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        final Map<String, String[]> fakedParams = Collections.singletonMap("sessionId",
                new String[] { httpRequest.getSession().getId() });
        HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(httpRequest) {
            @Override
            public Map<String, String[]> getParameterMap() {
                return fakedParams;
            }
        };
        chain.doFilter(wrappedRequest, response);
    }

    @Override
    public void init(FilterConfig config) throws ServletException { }
    @Override
    public void destroy() { }
}

WebSocketEndpoint.java

package example2;

import javax.servlet.http.HttpSession;
import javax.websocket.EndpointConfig;
import javax.websocket.HandshakeResponse;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;

@ServerEndpoint(value = "/example2", configurator = WebSocketEndpoint.Configurator.class)
public class WebSocketEndpoint {
    private HttpSession httpSession;

    @OnOpen
    public void onOpen(Session webSocketSession, EndpointConfig config) {
        String sessionId = webSocketSession.getRequestParameterMap().get("sessionId").get(0);
        SessionTracker tracker =
                (SessionTracker) config.getUserProperties().get(SessionTracker.class.getName());
        httpSession = tracker.getSessionById(sessionId);
    }

    @OnMessage
    public String demo(String msg) {
        return msg + "; (example 2) session ID " + httpSession.getId();
    }

    public static class Configurator extends ServerEndpointConfig.Configurator {
        @Override
        public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request,
                HandshakeResponse response) {
            Object tracker = ((HttpSession) request.getHttpSession()).getServletContext().getAttribute(
                    SessionTracker.class.getName());
            // This is safe to do because it's the same instance of SessionTracker all the time
            sec.getUserProperties().put(SessionTracker.class.getName(), tracker);
            super.modifyHandshake(sec, request, response);
        }
    }
}

Solution for Single Properties

If you only need certain properties out of the HttpSession and not the whole HttpSession itself, like say a user ID, then you could do away with the whole SessionTracker business and just put the necessary parameters in the map you return from your override of HttpServletRequestWrapper.getParameterMap(). Then you can also get rid of the custom Configurator; your properties will be conveniently accessible from Session.getRequestParameterMap() in the Endpoint.

WebSocketFilter.java

package example5;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

@WebFilter("/example5")
public class WebSocketFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        final Map<String, String[]> props = new HashMap<>();
        // Add properties of interest from session; session ID
        // is just for example
        props.put("sessionId", new String[] { httpRequest.getSession().getId() });
        HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(httpRequest) {
            @Override
            public Map<String, String[]> getParameterMap() {
                return props;
            }
        };
        chain.doFilter(wrappedRequest, response);
    }

    @Override
    public void destroy() {
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {
    }
}

WebSocketEndpoint.java

package example5;

import java.util.List;
import java.util.Map;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/example5")
public class WebSocketEndpoint {
    private Map<String, List<String>> params;

    @OnOpen
    public void onOpen(Session session) {
        params = session.getRequestParameterMap();
    }

    @OnMessage
    public String demo(String msg) {
        return msg + "; (example 5) session ID " + params.get("sessionId").get(0);
    }
}
Corrode answered 12/5, 2015 at 0:58 Comment(7)
This should be the accepted answer. Great work Chad. But I have a doubt: I don't think it is possible to place an outer filter for every implementationInternationalize
I tried this on Wildfly/Undertow, but session.getUserPrincipal() always returns null even if the filter is applied correctly. Is it possibile the Undertow WS doesn't use the servlet request from the filter?Ogle
"I've developed two solutions, which I've verified work correctly when tested against a multithreaded client." <--- tested on what plaforms?Anathematize
You cannot cast the WebSocket Session around like that (its not supported by the JSR356 spec), your solution will be implementation specific.Extreme
Also, the JSR356 spec has no support requirement of Filters before the WebSocket upgrade. The implementers can choose to handle Upgrade before the Servlet chain is executed. (and many implementation do this)Extreme
@Anathematize Works on Tomcat 8.0.23 and 9.0.0.M13; GlassFish 4.1.1; Jetty 9.1.6. However it has stopped working on Jetty 9.2.10 and 9.3.8 because it no longer processes your Servlet Filters before the WebSocket upgrade.Corrode
@JoakimErdfelt Where is the invalid cast? I am not sure which line you're referring to. This one Object tracker = ((HttpSession) request.getHttpSession())... should be OK by the HandshakeRequest JavaDoc. This one httpSession = ((PrincipalWithSession) webSocketSession.getUserPrincipal())... might be sketchier...it depends on the Principal being the same one returned by HttpServletRequest.getUserPrincipal()...but it is not casting a WS session.Corrode
T
7

Is it possible?

Let's review the Java API for WebSocket specification to see if getting hold of the HttpSession object is possible. The specification says on page 29:

Because websocket connections are initiated with an http request, there is an association between the HttpSession under which a client is operating and any websockets that are established within that HttpSession. The API allows access in the opening handshake to the unique HttpSession corresponding to that same client.

So yes it is possible.

However, I don't think it is possible for you to get hold of a reference to the HttpServletRequest object though. You could listen for all new servlet requests using a ServletRequestListener, but you would still have to figure out which request belong to which server endpoint. Please let me know if you find a solution!

Abstract how-to

How-to is loosely described on pages 13 and 14 in the specification and exemplified by me in code under the next heading.

In English, we will need to intercept the handshake process to get hold of a HttpSession object. To then transfer the HttpSession reference to our server endpoint, we also need to intercept when the container creates the server endpoint instance and manually inject the reference. We do all of this by providing our own ServerEndpointConfig.Configurator and override the methods modifyHandshake() and getEndpointInstance().

The custom configurator will be instantiated once per logical ServerEndpoint (See the JavaDoc).

Code example

This is the server endpoint class (I provide the implementation of the CustomConfigurator class after this code snippet):

@ServerEndpoint(value = "/myserverendpoint", configurator = CustomConfigurator.class)
public class MyServerEndpoint
{
    private HttpSession httpSession;

    public void setHttpSession(HttpSession httpSession) {
        if (this.httpSession != null) {
            throw new IllegalStateException("HttpSession has already been set!");
        }

        this.httpSession = httpSession;
    }

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        System.out.println("My Session Id: " + httpSession.getId());
    }
}

And this is the custom configurator:

public class CustomConfigurator extends ServerEndpointConfig.Configurator
{
    private HttpSession httpSession;

    // modifyHandshake() is called before getEndpointInstance()!
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        httpSession = (HttpSession) request.getHttpSession();
        super.modifyHandshake(sec, request, response);
    }

    @Override
    public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
        T endpoint = super.getEndpointInstance(endpointClass);

        if (endpoint instanceof MyServerEndpoint) {
            // The injection point:
            ((MyServerEndpoint) endpoint).setHttpSession(httpSession);
        }
        else {
            throw new InstantiationException(
                    MessageFormat.format("Expected instanceof \"{0}\". Got instanceof \"{1}\".",
                    MyServerEndpoint.class, endpoint.getClass()));
        }

        return endpoint;
    }
}
Trifle answered 31/7, 2013 at 11:39 Comment(6)
What is ConnectionWebSocket?Extreme
oh shit. Sorry! Copy pasted that part from an already implemented solution I had. In my case the endpoint was called a ConnectionWebSocket =) I edited the answer. Thank you! Don't forget to upvote if it was useful to you.Trifle
There is an easier (and more correct) way, see my answer to this question too.Extreme
For JBoss/WildFly 8.1 powered by Undertow, getEndpointInstance is never called at all. For me, the only workaround is to pass HttpSession via ThreadLocal because modifyHandshake and onOpen are actually executed in the same filter/request processing chain.Datolite
I was wrong, modifyHandshake and onOpen are always executed in different threads, so I had to pass servlet data via static map with keys as pairs host/port of the client connection. However, this information is not directly available but via some reflection magic.Datolite
That one is working fine for me so far, using jetty 9.3. Thanks.Hypso
C
4

All above answers worth reading, but none of them solves OP's (and my) problem.

You can access HttpSession when a WS end point is opening and pass it to newly created end point instance but no one guarantees there exist HttpSession instance!

So we need step 0 before this hacking (I hate JSR 365 implementation of WebSocket). Websocket - httpSession returns null

Climatology answered 28/11, 2014 at 5:59 Comment(1)
This is a good point, but it is not an answer and should be a comment.Creight
I
0

All possible solutions are based upon:

A. Client browser implementations maintains Session ID via Cookie value passed as an HTTP Header, or (if cookies are disabled) it is managed by Servlet container which will generate Session ID postfix for generated URLs

B. You can access HTTP Request Headers only during HTTP handshake; after that, it is Websocket Protocol

So that...

Solution 1: use "handshake" to access HTTP

Solution 2: In your JavaScript on the client side, dynamically generate HTTP Session ID parameter and send first message (via Websocket) containing this Session ID. Connect "endpoint" to the cache / utility class maintaining Session ID -> Session mapping; avoid memory leaks, you can use Session Listener for instance to remove session from cache.

P.S. I appreciate answers from Martin Andersson and Joakim Erdfelt. Unfortunately, solution of Martin is not thread safe...

Inchoate answered 17/9, 2014 at 17:1 Comment(2)
You can literally vote for solution 1 once you have 15 rep. This looks more like commentary, which you can earn the privilege of sharing once you have 50 rep. Please read "How do I write a good answer?"Rafaello
I was unable to comment, that's why I posted it as 'answer'. Thank you!Inchoate
T
0

The only way that works across all applications servers is use ThreadLocal. See:

https://java.net/jira/browse/WEBSOCKET_SPEC-235

Tooth answered 10/2, 2017 at 16:51 Comment(1)
link is dead. That's why it's best to write examples. Isn't it superseded by : oracle.com/technical-resources/articles/java/jsr356.html ?Mitten

© 2022 - 2024 — McMap. All rights reserved.