diff --git a/packages/client-python/src/rocketride/mixins/connection.py b/packages/client-python/src/rocketride/mixins/connection.py index 2524b2e0b..66315ed79 100644 --- a/packages/client-python/src/rocketride/mixins/connection.py +++ b/packages/client-python/src/rocketride/mixins/connection.py @@ -313,8 +313,11 @@ def _get_websocket_uri(uri: str) -> str: parsed = urllib.parse.urlparse(normalized) ws_scheme = 'wss' if parsed.scheme in ('https', 'wss') else 'ws' - ws_uri = parsed._replace(scheme=ws_scheme) - return f'{ws_uri.geturl()}/task/service' + path = parsed.path.rstrip('/') + if not path.endswith('/task/service'): + path = f'{path}/task/service' if path else '/task/service' + ws_uri = parsed._replace(scheme=ws_scheme, path=path) + return ws_uri.geturl() def _set_uri(self, uri: str) -> None: """Update the server URI (internal).""" diff --git a/packages/client-python/tests/test_connection_uri.py b/packages/client-python/tests/test_connection_uri.py new file mode 100644 index 000000000..320902ec6 --- /dev/null +++ b/packages/client-python/tests/test_connection_uri.py @@ -0,0 +1,22 @@ +import pytest + +from rocketride.mixins.connection import ConnectionMixin + + +@pytest.mark.parametrize( + ('input_uri', 'expected_uri'), + [ + ('http://localhost:5565', 'ws://localhost:5565/task/service'), + ('http://localhost:5565/', 'ws://localhost:5565/task/service'), + ('https://cloud.rocketride.ai', 'wss://cloud.rocketride.ai/task/service'), + ('https://cloud.rocketride.ai/', 'wss://cloud.rocketride.ai/task/service'), + ('wss://cloud.rocketride.ai', 'wss://cloud.rocketride.ai/task/service'), + ('wss://cloud.rocketride.ai/', 'wss://cloud.rocketride.ai/task/service'), + ('wss://cloud.rocketride.ai/task/service', 'wss://cloud.rocketride.ai/task/service'), + ('ws://localhost:5565', 'ws://localhost:5565/task/service'), + ('ws://localhost:5565/', 'ws://localhost:5565/task/service'), + ('ws://localhost:5565/task/service', 'ws://localhost:5565/task/service'), + ], +) +def test_get_websocket_uri_normalizes_task_service_path(input_uri, expected_uri): + assert ConnectionMixin._get_websocket_uri(input_uri) == expected_uri diff --git a/packages/client-typescript/src/client/client.ts b/packages/client-typescript/src/client/client.ts index b6678c756..ba552cfe2 100644 --- a/packages/client-typescript/src/client/client.ts +++ b/packages/client-typescript/src/client/client.ts @@ -435,13 +435,23 @@ export class RocketRideClient extends DAPClient { */ private _getWebsocketUri(uri: string): string { const httpUrl = RocketRideClient.normalizeUri(uri); + const SERVICE_PATH = '/task/service'; try { const url = new URL(httpUrl); const wsScheme = url.protocol === 'https:' || url.protocol === 'wss:' ? 'wss:' : 'ws:'; - return `${wsScheme}//${url.host}/task/service`; + // Normalize the path: strip trailing slash, then append /task/service + // only if it isn't already present (prevents double-path when callers + // pass an already-normalized WebSocket endpoint). + let path = url.pathname.replace(/\/+$/, ''); + if (!path.endsWith(SERVICE_PATH)) { + path = path ? `${path}${SERVICE_PATH}` : SERVICE_PATH; + } + return `${wsScheme}//${url.host}${path}`; } catch { - return `${httpUrl}/task/service`; + // Fallback for unparseable URIs — same trailing-slash + dedup logic + const stripped = httpUrl.replace(/\/+$/, ''); + return stripped.endsWith(SERVICE_PATH) ? stripped : `${stripped}${SERVICE_PATH}`; } }