Can I access request cookies in a Bokeh server app?

Hi there,

By way of context, I run my app using bokeh serve app_dir and what I’m trying to do is

  1. add a drop down to a Bokeh app that can be used to select one of a number of themes
  2. have the user selection remembered using a cookie for the next time they visit the web page

I’ve managed to achieve 1. using a Select object with a callback, and part of 2. by setting a cookie on the webpage with a CustomJS callback e.g. document.cookie = "theme=dark";

What I’m struggling with though is viewing the cookie from the HTTP request the next time the user visits the page. If I call

request = curdoc().session_context.request

then I get a _RequestProxy object which only has a copy of the query parameters as arguments on it, it doesn’t have a copy of the cookies that were on the request.

I can patch Bokeh and the _RequestProxy object to also contain a copy of the cookies but before I do that I’d like to know

  • Is there an official, supported way of getting access to the request cookies?
  • Is there an alternative mechanism to do what I want? I’d like to avoid saving the preference server side as then it would need to be communicated between server instances.
  • If no to both of the above, are there any downsides of taking a copy of the request cookies and putting them on the _RequestProxy object?

Many thanks

Alan

Well the short answer is that there is not a good way currently. Regarding:

If no to both of the above, are there any downsides of taking a copy of the request cookies and putting them on the _RequestProxy object?

Only the downside that prompted _RequestProxy in the first place: if you run tornado with --num-threads > 1, or run multiple Bokeh servers behind a load balancer without sticky sessions, then there is no guarantee that the initial HTTP request and the secondary websocket request end up on the same process. In which case, trying to access the HTTP request directly in the app code will cause a crash. We manually forward just the request args through to the WS request, and explicitly make them available on the _RequestProxy. You’d need to do something similar if your use case is more than just a single Bokeh server.

I would however like to improve things for Bokeh 2.0 in the next couple of months. In particular the “session id” that we send as a query argument is really basically a web token, so I’d like to actually utilize that capability, and encode more of the HTTP request inside the token, as well as put in hooks for users to add any other data they want in the token. Ideally I’d like to get the token out of they query arguments as well, as a secondary consideration. [1]

So, if you can wait a little while, there should be a better solution before the end of the year. (And if you have the ability to help with some of this, perhaps even sooner.)


  1. The main issue here is on the websocket side, where there are only very limited supported headers. But abusing the subprotocol header to pass bearer tokens seems to be a fairly common and accepted workaround solution. ↩︎

Hi Bryan,

Thanks for the reply. I have patched the _RequestProxy class locally to be the following

class _RequestProxy(object):
    def __init__(self, request):
        args_copy = dict(request.arguments)
        cookies_copy = dict(request.cookies)
        if 'bokeh-protocol-version' in args_copy: del args_copy['bokeh-protocol-version']
        if 'bokeh-session-id' in args_copy: del args_copy['bokeh-session-id']
        self._args = args_copy
        self._cookies = cookies_copy
    @property
    def arguments(self):
        return self._args

    @property
    def cookies(self):
        return self._cookies

and so far that seems to work. I think I get your concern about the HTTP GET request and WebSockets request going to different servers but from what I can see (checking with Chrome and Firefox dev tools) the Cookie header is sent on the Websocket connection, although this is outside my area of expertise so perhaps it isn’t guaranteed?

I’ll keep checking with my local usecase. Also, off to try an build and run the Bokeh test cases to see if I’ve broken anything. :slight_smile:

Regards

Alan

Just FYI the above works because a session is created on HTTP connection, and with only one Bokeh server process, when the WebSocket connection lands back on the same process, that same previous session is re-used, with the same literal _RequestProxy object available.

Perhaps browsers have support for non-standard behaviors? I’m basing my statement on e.g.

AFAIK Sec-WebSocket-Protocol is the only header sent. Which means the only reliable ways to get other information to WS connections are:

  • Encode them in query arguments. This is what we do now with the original HTTP request arguments. But we stopped with just that. In principle, anything you want could be added there, including cookies. (But transmitting cookies in easily viewable query arguments seems like a bad idea)

  • Slightly abuse the Sec-WebSocket-Protocol to send extra information, i.e. in a JWT-like token. This is what I intend to do for Bokeh 2.0

I’ll keep checking with my local usecase. Also, off to try an build and run the Bokeh test cases to see if I’ve broken anything.

The only test that might break would be a strict test to make sure _RequestProxy does not have anything extra on unexpected on it. I don’t recall offhand if we made such a test.