diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 6ee96fe2..13f835d0 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1692,7 +1692,11 @@ class JupyterHub(Application): """add a url prefix to handlers""" for i, tup in enumerate(handlers): lis = list(tup) - lis[0] = url_path_join(prefix, tup[0]) + if tup[0]: + lis[0] = url_path_join(prefix, tup[0]) + else: + # the '' route should match /prefix not /prefix/ + lis[0] = prefix.rstrip("/") handlers[i] = tuple(lis) return handlers diff --git a/jupyterhub/tests/test_utils.py b/jupyterhub/tests/test_utils.py index 7d17b1dc..aefa9e30 100644 --- a/jupyterhub/tests/test_utils.py +++ b/jupyterhub/tests/test_utils.py @@ -100,6 +100,29 @@ async def test_tornado_coroutines(): assert (await t.tornado_coroutine()) == "gen.coroutine" +@pytest.mark.parametrize( + "pieces, expected", + [ + (("/"), "/"), + (("/", "/"), "/"), + (("/base", ""), "/base"), + (("/base/", ""), "/base/"), + (("/base", "abc", "def"), "/base/abc/def"), + (("/base/", "/abc/", "/def/"), "/base/abc/def/"), + (("/base", "", "/", ""), "/base/"), + ((""), ""), + (("", ""), ""), + (("", "part", ""), "part"), + (("", "/part"), "part"), + (("", "part", "", "after"), "part/after"), + (("", "part", "", "after/", "", ""), "part/after/"), + (("abc", "def"), "abc/def"), + ], +) +def test_url_path_join(pieces, expected): + assert utils.url_path_join(*pieces) == expected + + @pytest.mark.parametrize( "forwarded, x_scheme, x_forwarded_proto, expected", [ diff --git a/jupyterhub/user.py b/jupyterhub/user.py index aba33720..1529b515 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -786,7 +786,7 @@ class User: if handler: await self.refresh_auth(handler) - base_url = url_path_join(self.base_url, url_escape_path(server_name)) + '/' + base_url = url_path_join(self.base_url, url_escape_path(server_name), "/") orm_server = orm.Server(base_url=base_url) db.add(orm_server) @@ -877,8 +877,7 @@ class User: api_token, url_path_join(self.url, url_escape_path(server_name), 'oauth_callback'), allowed_scopes=allowed_scopes, - description="Server at %s" - % (url_path_join(self.base_url, server_name) + '/'), + description=f"Server at {url_path_join(self.base_url, server_name, '/')}", ) spawner.orm_spawner.oauth_client = oauth_client db.commit() diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index 54c2208c..e3d2ddf8 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -471,9 +471,15 @@ def url_path_join(*pieces): Use to prevent double slash when joining subpath. This will leave the initial and final / in place. + Empty trailing items are ignored. - Copied from `notebook.utils.url_path_join`. + Based on `notebook.utils.url_path_join`. """ + pieces = list(pieces) + while pieces and not pieces[-1]: + del pieces[-1] + if not pieces: + return "" initial = pieces[0].startswith('/') final = pieces[-1].endswith('/') stripped = [s.strip('/') for s in pieces]