Repository: bokeh/bokeh

Issue:
pull_session with url paramters isn't working
I'm trying to call `pull_session` from my flask app.
bokeh version is `0.12.15`

```
session = pull_session(url=f'http://localhost:5006/my_app?plot=scatter')
```

The bokeh server is processing the request normaly, but i'm having this error
```
OSError: Cannot pull session document because we failed to connect to the server (to start the server, try the 'bokeh serve' command)
```
Without the url parameters everything is working great

_Bokeh server_
```
from tornado.ioloop import IOLoop
from bokeh.server.server import Server
from bokeh.layouts import widgetbox, row
from bokeh.models.widgets import Slider
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
import numpy as np


def sliders_modify_doc(curdoc):
    # Set up data
    N = 200
    x = np.linspace(0, 4 * np.pi, N)
    y = np.sin(x)
    source = ColumnDataSource(data=dict(x=x, y=y))

    # Set up widgets
    offset = Slider(title="offset", value=0.0, start=-5.0, end=5.0, step=0.1)
    amplitude = Slider(title="amplitude", value=1.0, start=-5.0, end=5.0, step=0.1)
    phase = Slider(title="phase", value=0.0, start=0.0, end=2 * np.pi)
    freq = Slider(title="frequency", value=1.0, start=0.1, end=5.1, step=0.1)

    # Set up plot
    plot = figure(plot_height=400, plot_width=400, title="my sine wave",
                  tools="crosshair,pan,reset,save,wheel_zoom",
                  x_range=[0, 4 * np.pi], y_range=[-2.5, 2.5])

    plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

    def update_data(attrname, old, new):
        # Get the current slider values
        a = amplitude.value
        b = offset.value
        w = phase.value
        k = freq.value

        # Generate the new curve
        x = np.linspace(0, 4 * np.pi, N)
        y = a * np.sin(k * x + w) + b

        source.data = dict(x=x, y=y)

    for w in [offset, amplitude, phase, freq]:
        w.on_change('value', update_data)

    inputs = widgetbox(offset, amplitude, phase, freq)
    curdoc.add_root(row(inputs, plot, width=800))
    
def start_bokeh_worker():
    server = Server({'/my_app': sliders_modify_doc},
                    io_loop=IOLoop(),
                    allow_websocket_origin=["localhost:5000"])

    server.start()
    server.io_loop.start()


if __name__ == '__main__':
    start_bokeh_worker()
```

_Flask server_
```
from flask import Flask, render_template
from bokeh.client import pull_session
from bokeh.embed import server_session

app = Flask(__name__, template_folder='templates')

@app.route('/bokeh')
def get():
    session = pull_session(url=f'http://localhost:5006/my_app?plot=scatter')
    script = server_session(None, session.id, url='http://localhost:5006/my_app')

    return render_template("bokeh.html", script=script, template="Flask")

if __name__ == '__main__':
    app.run()
```
_bokeh.html_
```
<html>
    <body>
      {{ script|safe }}
    </body>
</html>
```

Also, the last parameter has a suffix (when no parameters are given `request.arguments` is empty
![image](https://user-images.githubusercontent.com/5768348/38633130-64aff7f0-3dc7-11e8-8ce9-a672dd8203e8.png)



Discussion:
@ericman93 the issue template asked for quite a bit more information, including versions, and a minimal complete reproducer. Can you provide this information? If not, we will have to close this as `norepro`
Just updated to issue :+1: 
Thanks, still needing bokeh version information. Also I tried to run the code and it is missing imports.  I realize this may seem like harping on small points but there there are 100k's of users, and only 1-2 core devs, so missing small things quickly add up to an unmanagable burden for a very few people.
bokeh version is `0.12.15` and the code is fixed :+1: 
@ericman93 thanks this was reproducible, which means is should be possible to do something about it. Guessing it was just overlooking in the other PR.
Im struggling with similar issues. I hope they fix this one soon too 

Resolving patch:
diff --git a/bokeh/client/connection.py b/bokeh/client/connection.py
--- a/bokeh/client/connection.py
+++ b/bokeh/client/connection.py
@@ -28,6 +28,7 @@
 # Standard library imports
 
 # External imports
+from six.moves.urllib.parse import quote_plus
 from tornado import gen
 from tornado.httpclient import HTTPRequest
 from tornado.ioloop import IOLoop
@@ -57,12 +58,13 @@ class ClientConnection(object):
 
     '''
 
-    def __init__(self, session, websocket_url, io_loop=None):
+    def __init__(self, session, websocket_url, io_loop=None, arguments=None):
         ''' Opens a websocket connection to the server.
 
         '''
         self._url = websocket_url
         self._session = session
+        self._arguments = arguments
         self._protocol = Protocol("1.0")
         self._receiver = Receiver(self._protocol)
         self._socket = None
@@ -229,9 +231,16 @@ def send_message(self, message):
 
     # Private methods ---------------------------------------------------------
 
+    def _versioned_url(self):
+        versioned_url = "%s?bokeh-protocol-version=1.0&bokeh-session-id=%s" % (self._url, self._session.id)
+        if self._arguments is not None:
+            for key, value in self._arguments.items():
+                versioned_url += "&{}={}".format(quote_plus(str(key)), quote_plus(str(value)))
+        return versioned_url
+
     @gen.coroutine
     def _connect_async(self):
-        versioned_url = "%s?bokeh-protocol-version=1.0&bokeh-session-id=%s" % (self._url, self._session.id)
+        versioned_url = self._versioned_url()
         request = HTTPRequest(versioned_url)
         try:
             socket = yield websocket_connect(request)
diff --git a/bokeh/client/session.py b/bokeh/client/session.py
--- a/bokeh/client/session.py
+++ b/bokeh/client/session.py
@@ -63,7 +63,7 @@
 # General API
 #-----------------------------------------------------------------------------
 
-def pull_session(session_id=None, url='default', io_loop=None):
+def pull_session(session_id=None, url='default', io_loop=None, arguments=None):
     ''' Create a session by loading the current server-side document.
 
     ``session.document`` will be a fresh document loaded from
@@ -103,13 +103,22 @@ def pull_session(session_id=None, url='default', io_loop=None):
         io_loop (``tornado.ioloop.IOLoop``, optional) :
             The IOLoop to use for the websocket
 
+        arguments (dict[str, str], optional) :
+            A dictionary of key/values to be passed as HTTP request arguments
+            to Bokeh application code (default: None)
+
+            Note that should only be provided when pulling new sessions.
+            If ``session_id`` is not None, or a session with ``session_id``
+            already exists, these arguments will have no effect.
+
     Returns:
         ClientSession :
             A new ClientSession connected to the server
 
     '''
+
     coords = _SessionCoordinates(session_id=session_id, url=url)
-    session = ClientSession(session_id=session_id, websocket_url=websocket_url_for_server_url(coords.url), io_loop=io_loop)
+    session = ClientSession(session_id=session_id, websocket_url=websocket_url_for_server_url(coords.url), io_loop=io_loop, arguments=arguments)
     session.pull()
     return session
 
@@ -225,7 +234,7 @@ class ClientSession(object):
 
     '''
 
-    def __init__(self, session_id=None, websocket_url=DEFAULT_SERVER_WEBSOCKET_URL, io_loop=None):
+    def __init__(self, session_id=None, websocket_url=DEFAULT_SERVER_WEBSOCKET_URL, io_loop=None, arguments=None):
         ''' A connection which attaches to a particular named session on the
         server.
 
@@ -247,12 +256,20 @@ def __init__(self, session_id=None, websocket_url=DEFAULT_SERVER_WEBSOCKET_URL,
             io_loop (IOLoop, optional) :
                 The IOLoop to use for the websocket
 
+            arguments (dict[str, str], optional) :
+                A dictionary of key/values to be passed as HTTP request
+                arguments to Bokeh application code (default: None)
+
+                Note that should only be provided when pulling new sessions.
+                If ``session_id`` is not None, or a session with ``session_id``
+                already exists, these arguments will have no effect.
+
         '''
         self._document = None
         self._id = self._ensure_session_id(session_id)
 
         from .connection import ClientConnection
-        self._connection = ClientConnection(session=self, io_loop=io_loop, websocket_url=websocket_url)
+        self._connection = ClientConnection(session=self, io_loop=io_loop, websocket_url=websocket_url, arguments=arguments)
 
         from ..server.callbacks import _DocumentCallbackGroup
         self._callbacks = _DocumentCallbackGroup(self._connection.io_loop)


Test patch:
diff --git a/bokeh/client/tests/test_connection.py b/bokeh/client/tests/test_connection.py
--- a/bokeh/client/tests/test_connection.py
+++ b/bokeh/client/tests/test_connection.py
@@ -32,6 +32,9 @@
 # Setup
 #-----------------------------------------------------------------------------
 
+class FakeSess(object):
+    id = "session_id"
+
 #-----------------------------------------------------------------------------
 # General API
 #-----------------------------------------------------------------------------
@@ -50,8 +53,29 @@ def test_creation(self):
 
         assert c._session == "session"
         assert isinstance(c._state, NOT_YET_CONNECTED)
-        assert c._until_predicate == None
-        assert c._server_info == None
+        assert c._until_predicate is None
+        assert c._server_info is None
+        assert c._arguments is None
+
+    def test_creation_with_arguments(self):
+        c = bcc.ClientConnection("session", "wsurl", arguments=dict(foo="bar"))
+        assert c.url == "wsurl"
+        assert c.connected == False
+        assert isinstance(c.io_loop, IOLoop)
+
+        assert c._session == "session"
+        assert isinstance(c._state, NOT_YET_CONNECTED)
+        assert c._until_predicate is None
+        assert c._server_info is None
+        assert c._arguments == dict(foo="bar")
+
+    def test__versioned_url(self):
+        c = bcc.ClientConnection(FakeSess(), "wsurl")
+        assert c._versioned_url() == "wsurl?bokeh-protocol-version=1.0&bokeh-session-id=session_id"
+
+    def test__versioned_url_with_arguments(self):
+        c = bcc.ClientConnection(FakeSess(), "wsurl", arguments=dict(foo="bar"))
+        assert c._versioned_url() == "wsurl?bokeh-protocol-version=1.0&bokeh-session-id=session_id&foo=bar"
 
 #-----------------------------------------------------------------------------
 # Private API
diff --git a/bokeh/client/tests/test_session.py b/bokeh/client/tests/test_session.py
--- a/bokeh/client/tests/test_session.py
+++ b/bokeh/client/tests/test_session.py
@@ -51,6 +51,7 @@ def test_creation_defaults(self):
         s = bcs.ClientSession()
         assert s.connected == False
         assert s.document is None
+        assert s._connection._arguments is None
         assert isinstance(s.id, string_types)
         assert len(s.id) == 44
 
@@ -58,12 +59,14 @@ def test_creation_with_session_id(self):
         s = bcs.ClientSession("sid")
         assert s.connected == False
         assert s.document is None
+        assert s._connection._arguments is None
         assert s.id == "sid"
 
     def test_creation_with_ws_url(self):
         s = bcs.ClientSession(websocket_url="wsurl")
         assert s.connected == False
         assert s.document is None
+        assert s._connection._arguments is None
         assert s._connection.url == "wsurl"
         assert isinstance(s.id, string_types)
         assert len(s.id) == 44
@@ -72,10 +75,18 @@ def test_creation_with_ioloop(self):
         s = bcs.ClientSession(io_loop="io_loop")
         assert s.connected == False
         assert s.document is None
+        assert s._connection._arguments is None
         assert s._connection.io_loop == "io_loop"
         assert isinstance(s.id, string_types)
         assert len(s.id) == 44
 
+    def test_creation_with_arguments(self):
+        s = bcs.ClientSession(arguments="args")
+        assert s.connected == False
+        assert s.document is None
+        assert s._connection._arguments == "args"
+        assert len(s.id) == 44
+
     @patch("bokeh.client.connection.ClientConnection.connect")
     def test_connect(self, mock_connect):
         s = bcs.ClientSession()