From d6dd8e734b4f30a9d94386de70c143ef9c5df073 Mon Sep 17 00:00:00 2001 From: Brian Ball Date: Mon, 11 Aug 2025 16:23:26 -0400 Subject: [PATCH 1/2] Fix when base_url is used in combination with a gateway client. --- jupyter_server/gateway/gateway_client.py | 19 ++++++++++++++++--- jupyter_server/gateway/managers.py | 5 ++++- tests/test_gateway.py | 24 +++++++++++++++++++----- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/jupyter_server/gateway/gateway_client.py b/jupyter_server/gateway/gateway_client.py index 966cf03f35..16b070fab5 100644 --- a/jupyter_server/gateway/gateway_client.py +++ b/jupyter_server/gateway/gateway_client.py @@ -33,6 +33,7 @@ from traitlets.config import LoggingConfigurable, SingletonConfigurable from jupyter_server import DEFAULT_EVENTS_SCHEMA_PATH, JUPYTER_SERVER_EVENTS_URI +from jupyter_server.utils import url_path_join ERROR_STATUS = "error" SUCCESS_STATUS = "success" @@ -170,6 +171,18 @@ def _ws_url_validate(self, proposal): raise TraitError(message) return value + base_url = Unicode( + default_value="/", + config=True, + help="""The gateway API base_url for fixing default kernel endpoints""", + ) + + @observe("base_url") + def _base_url(self, change): + self.kernels_endpoint = self._kernels_endpoint_default() + self.kernelspecs_endpoint = self._kernelspecs_endpoint_default() + self.kernelspecs_resource_endpoint = self._kernelspecs_resource_endpoint_default() + kernels_endpoint_default_value = "/api/kernels" kernels_endpoint_env = "JUPYTER_GATEWAY_KERNELS_ENDPOINT" kernels_endpoint = Unicode( @@ -180,7 +193,7 @@ def _ws_url_validate(self, proposal): @default("kernels_endpoint") def _kernels_endpoint_default(self): - return os.environ.get(self.kernels_endpoint_env, self.kernels_endpoint_default_value) + return os.environ.get(self.kernels_endpoint_env, url_path_join(self.base_url, self.kernels_endpoint_default_value)) kernelspecs_endpoint_default_value = "/api/kernelspecs" kernelspecs_endpoint_env = "JUPYTER_GATEWAY_KERNELSPECS_ENDPOINT" @@ -193,7 +206,7 @@ def _kernels_endpoint_default(self): @default("kernelspecs_endpoint") def _kernelspecs_endpoint_default(self): return os.environ.get( - self.kernelspecs_endpoint_env, self.kernelspecs_endpoint_default_value + self.kernelspecs_endpoint_env, url_path_join(self.base_url, self.kernelspecs_endpoint_default_value) ) kernelspecs_resource_endpoint_default_value = "/kernelspecs" @@ -209,7 +222,7 @@ def _kernelspecs_endpoint_default(self): def _kernelspecs_resource_endpoint_default(self): return os.environ.get( self.kernelspecs_resource_endpoint_env, - self.kernelspecs_resource_endpoint_default_value, + url_path_join(self.base_url, self.kernelspecs_resource_endpoint_default_value), ) connect_timeout_default_value = 40.0 diff --git a/jupyter_server/gateway/managers.py b/jupyter_server/gateway/managers.py index daa6f99213..2d1c05b69d 100644 --- a/jupyter_server/gateway/managers.py +++ b/jupyter_server/gateway/managers.py @@ -54,6 +54,7 @@ def _default_shared_context(self): def __init__(self, **kwargs): """Initialize a gateway mapping kernel manager.""" super().__init__(**kwargs) + GatewayClient.instance().base_url = self.parent.base_url self.kernels_url = url_path_join( GatewayClient.instance().url or "", GatewayClient.instance().kernels_endpoint or "" ) @@ -218,6 +219,8 @@ class GatewayKernelSpecManager(KernelSpecManager): def __init__(self, **kwargs): """Initialize a gateway kernel spec manager.""" super().__init__(**kwargs) + GatewayClient.instance().base_url = self.parent.base_url + base_endpoint = url_path_join( GatewayClient.instance().url or "", GatewayClient.instance().kernelspecs_endpoint ) @@ -248,7 +251,7 @@ def _replace_path_kernelspec_resources(self, kernel_specs): resources = kernelspecs[kernel_name]["resources"] for resource_name in resources: original_path = resources[resource_name] - split_eg_base_url = str.rsplit(original_path, sep="/kernelspecs/", maxsplit=1) + split_eg_base_url = str.rsplit(original_path, sep="/kernelspecs", maxsplit=1) if len(split_eg_base_url) > 1: new_path = url_path_join( self.parent.base_url, "kernelspecs", split_eg_base_url[1] diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 00aa64f111..e260cb736f 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -28,13 +28,14 @@ from jupyter_server.gateway.gateway_client import GatewayTokenRenewerBase, NoOpTokenRenewer from jupyter_server.gateway.managers import ChannelQueue, GatewayClient, GatewayKernelManager from jupyter_server.services.kernels.websocket import KernelWebsocketHandler +from jupyter_server.utils import url_path_join from .utils import expected_http_error pytest_plugins = ["jupyter_events.pytest_plugin"] -def generate_kernelspec(name): +def generate_kernelspec(name, kernelspecs_endpoint): argv_stanza = ["python", "-m", "ipykernel_launcher", "-f", "{connection_file}"] spec_stanza = { "spec": { @@ -52,6 +53,7 @@ def generate_kernelspec(name): "resources": { "logo-64x64": f"f/kernelspecs/{name}/logo-64x64.png", "url": "https://example.com/example-url", + "kernelspec": kernelspecs_endpoint, }, } return kernelspec_stanza @@ -61,8 +63,9 @@ def generate_kernelspec(name): kernelspecs: dict = { "default": "kspec_foo", "kernelspecs": { - "kspec_foo": generate_kernelspec("kspec_foo"), - "kspec_bar": generate_kernelspec("kspec_bar"), + "kspec_foo": generate_kernelspec("kspec_foo", "/foo/kernelspecs"), + "kspec_bar": generate_kernelspec("kspec_bar", "/bar/kernelspecs/"), + "kspec_baz": generate_kernelspec("kspec_baz", GatewayClient.kernelspecs_endpoint_default_value), }, } @@ -437,11 +440,20 @@ async def test_gateway_get_kernelspecs(init_gateway, jp_fetch, jp_serverapp): assert r.code == 200 content = json.loads(r.body.decode("utf-8")) kspecs = content.get("kernelspecs") - assert len(kspecs) == 2 + assert len(kspecs) == 3 assert kspecs.get("kspec_bar").get("name") == "kspec_bar" assert ( kspecs.get("kspec_bar").get("resources")["logo-64x64"].startswith(jp_serverapp.base_url) ) + assert ( + kspecs.get("kspec_bar").get("resources")["kernelspec"].startswith(jp_serverapp.base_url) + ) + assert ( + kspecs.get("kspec_foo").get("resources")["kernelspec"].startswith(jp_serverapp.base_url) + ) + assert ( + kspecs.get("kspec_baz").get("resources")["kernelspec"].startswith(jp_serverapp.base_url) + ) async def test_gateway_get_named_kernelspec(init_gateway, jp_fetch): @@ -751,7 +763,9 @@ async def test_websocket_connection_with_session_id(init_gateway, jp_serverapp, await conn.connect() assert conn.session_id != None expected_ws_url = ( - f"{mock_gateway_ws_url}/api/kernels/{kernel_id}/channels?session_id={conn.session_id}" + url_path_join(mock_gateway_ws_url, + jp_serverapp.base_url, + f"/api/kernels/{kernel_id}/channels?session_id={conn.session_id}") ) assert ( expected_ws_url in caplog.text From a138cedf8f763cf7fcf74969a954c1e6e28610bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 20:57:29 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- jupyter_server/gateway/gateway_client.py | 8 ++++++-- tests/test_gateway.py | 12 +++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/jupyter_server/gateway/gateway_client.py b/jupyter_server/gateway/gateway_client.py index 16b070fab5..5efad4a270 100644 --- a/jupyter_server/gateway/gateway_client.py +++ b/jupyter_server/gateway/gateway_client.py @@ -193,7 +193,10 @@ def _base_url(self, change): @default("kernels_endpoint") def _kernels_endpoint_default(self): - return os.environ.get(self.kernels_endpoint_env, url_path_join(self.base_url, self.kernels_endpoint_default_value)) + return os.environ.get( + self.kernels_endpoint_env, + url_path_join(self.base_url, self.kernels_endpoint_default_value), + ) kernelspecs_endpoint_default_value = "/api/kernelspecs" kernelspecs_endpoint_env = "JUPYTER_GATEWAY_KERNELSPECS_ENDPOINT" @@ -206,7 +209,8 @@ def _kernels_endpoint_default(self): @default("kernelspecs_endpoint") def _kernelspecs_endpoint_default(self): return os.environ.get( - self.kernelspecs_endpoint_env, url_path_join(self.base_url, self.kernelspecs_endpoint_default_value) + self.kernelspecs_endpoint_env, + url_path_join(self.base_url, self.kernelspecs_endpoint_default_value), ) kernelspecs_resource_endpoint_default_value = "/kernelspecs" diff --git a/tests/test_gateway.py b/tests/test_gateway.py index e260cb736f..4cf4fc2a12 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -65,7 +65,9 @@ def generate_kernelspec(name, kernelspecs_endpoint): "kernelspecs": { "kspec_foo": generate_kernelspec("kspec_foo", "/foo/kernelspecs"), "kspec_bar": generate_kernelspec("kspec_bar", "/bar/kernelspecs/"), - "kspec_baz": generate_kernelspec("kspec_baz", GatewayClient.kernelspecs_endpoint_default_value), + "kspec_baz": generate_kernelspec( + "kspec_baz", GatewayClient.kernelspecs_endpoint_default_value + ), }, } @@ -762,10 +764,10 @@ async def test_websocket_connection_with_session_id(init_gateway, jp_serverapp, handler.connection = conn await conn.connect() assert conn.session_id != None - expected_ws_url = ( - url_path_join(mock_gateway_ws_url, - jp_serverapp.base_url, - f"/api/kernels/{kernel_id}/channels?session_id={conn.session_id}") + expected_ws_url = url_path_join( + mock_gateway_ws_url, + jp_serverapp.base_url, + f"/api/kernels/{kernel_id}/channels?session_id={conn.session_id}", ) assert ( expected_ws_url in caplog.text