Skip to content

fix(client): normalize WebSocket URI path to prevent double suffix (#737)#941

Open
Ramreddy2748 wants to merge 1 commit into
rocketride-org:developfrom
Ramreddy2748:fix/737-uri-normalization
Open

fix(client): normalize WebSocket URI path to prevent double suffix (#737)#941
Ramreddy2748 wants to merge 1 commit into
rocketride-org:developfrom
Ramreddy2748:fix/737-uri-normalization

Conversation

@Ramreddy2748
Copy link
Copy Markdown

@Ramreddy2748 Ramreddy2748 commented May 20, 2026

Problem

_get_websocket_uri concatenated /task/service unconditionally. Two failure modes:

  1. Trailing slashhttps://localhost:5565/ produced ws://localhost:5565//task/service (double-slash, rejected by the server).
  2. Path already presenthttps://localhost:5565/task/service produced ws://localhost:5565/task/service/task/service (duplicated path, connection hung).

Fix

  • Strip trailing slashes from the parsed path before checking.
  • Append the suffix only when the path does not already end with it.
  • Extract the suffix string into a module-level constant _TASK_SERVICE_SUFFIX to avoid magic strings.

Test plan

  • client.connect("localhost:5565")ws://localhost:5565/task/service
  • client.connect("localhost:5565/")ws://localhost:5565/task/service ✓ (was double-slash)
  • client.connect("localhost:5565/task/service")ws://localhost:5565/task/service ✓ (was duplicated path)
  • client.connect("https://cloud.rocketride.ai")wss://cloud.rocketride.ai/task/service

Closes #737

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Breaking Changes

    • Connection API simplified: attach()/detach()/login()/logout() methods removed; use connect()/disconnect() instead
    • Removed state-query methods: is_attached(), is_authenticated(), get_account_info()
  • New Features

    • Added set_connection_params() to update connection credentials without full reconnection
    • Enhanced reconnection with exponential backoff and automatic retry on connection loss

Review Change Stack

…ocketride-org#737)

_get_websocket_uri previously concatenated /task/service unconditionally,
producing double-slash or a duplicated path segment when the caller passed
a URI with a trailing slash or an already-correct path.

Now strips trailing slashes then appends the suffix only when absent,
and extracts the suffix string into a module-level constant.
@github-actions github-actions Bot added the module:client-python Python SDK and MCP client label May 20, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

ConnectionMixin was refactored to replace the prior attach/login/detach lifecycle with a simpler connect/disconnect pattern driven by explicit URI and auth parameters. Persist-mode reconnection now uses a single async retry task with exponential backoff and explicit authentication failure handling. URI normalization was fixed to prevent duplicate /task/service paths while maintaining scheme conversion.

Changes

Connection Lifecycle Refactoring

Layer / File(s) Summary
Foundation: documentation, imports, and state initialization
packages/client-python/src/rocketride/mixins/connection.py
Module docstring expanded to document new connection patterns and persist mode. Imports adjusted for time-based retry backoff and URI handling. __init__ refactored to track reconnection state (_manual_disconnect, _retry_delay, _retry_start_time, _max_retry_time, _reconnect_task), and callbacks (on_connected, on_disconnected) now gate reconnection scheduling based on persist mode and explicit disconnect status.
Reconnection engine with exponential backoff
packages/client-python/src/rocketride/mixins/connection.py
Internal _internal_connect / _internal_disconnect manage WebSocket transport lifecycle. _attempt_connection distinguishes AuthenticationException (stops retry, calls on_connect_error) from other failures (updates backoff delay). _schedule_reconnect and _attempt_reconnect handle async task scheduling with optional max_retry_time cutoff and task cancellation.
Public connection lifecycle API
packages/client-python/src/rocketride/mixins/connection.py
connect(uri, auth, timeout) method accepts URI and auth directly, disconnects if already connected (idempotent), and schedules reconnect on failure in persist mode. disconnect() cancels pending reconnect tasks and prevents auto-reconnect during teardown. New set_connection_params(uri, auth) updates connection parameters, tears down existing connection without triggering auto-reconnect, and conditionally reconnects based on persist mode and prior connection status. request method overrides parent to delegate requests.
URI normalization and connection helpers
packages/client-python/src/rocketride/mixins/connection.py
normalize_uri converts HTTP(S) schemes to WS(S) without explicit missing-host validation. _get_websocket_uri appends /task/service suffix only when missing (preventing double-slash and path duplication per issue #737). get_connection_info and get_apikey inspectors rely on updated internal fields.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • rocketride-org/rocketride-server#326: Modifies ConnectionMixin._get_websocket_uri URI normalization logic; this PR's connection URI handling overlaps with the retrieved PR's scheme-preservation behavior.

Suggested labels

module:client-python

Suggested reviewers

  • jmaionchi
  • stepmikhaylov
  • Rod-Christensen

Poem

🐰 The connection hops through time,
Backoff, retry, the dance sublime—
No double slashes spoil the path,
Auth fails once, no wrath.
Connect, disconnect, persist—
A cleaner way! We can't resist! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Major out-of-scope refactoring included: entire connection lifecycle redesign replacing attach/login/detach with connect/disconnect, plus new set_connection_params method. Separate the URI normalization fix (issue #737) from the broader connection lifecycle refactoring into distinct pull requests.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: URI path normalization to prevent double suffix, which is the primary change.
Linked Issues check ✅ Passed The implementation addresses all requirements from issue #737: prevents double slashes, prevents path duplication, and preserves correct resolution for host-only inputs.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

No description provided.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client-python/src/rocketride/mixins/connection.py`:
- Around line 386-424: The method set_connection_params can leave
self._manual_disconnect True if _internal_connect() or _schedule_reconnect()
raises; wrap the post-teardown reconnect logic (the block after setting
self._transport = None that calls self._schedule_reconnect() or
self._internal_connect()) in a try/finally so that self._manual_disconnect is
always reset to False in the finally clause; ensure the try covers both the
persist and non-persist branches (i.e., the await self._schedule_reconnect() and
await self._internal_connect() calls) and does not swallow exceptions (re-raise
after finally).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef0540ef-bea0-42da-be2d-5a9ad6db967d

📥 Commits

Reviewing files that changed from the base of the PR and between 3cf1937 and 388ae17.

📒 Files selected for processing (1)
  • packages/client-python/src/rocketride/mixins/connection.py

Comment on lines +386 to +424
async def set_connection_params(
self,
uri: Optional[str] = None,
auth: Optional[str] = None,
) -> None:
"""
Update server URI and/or auth. If currently connected, disconnects and
reconnects with the new params. In persist mode, reconnection is scheduled
only if we were connected (no auto-connect when params are set on a never-connected client).
In non-persist mode, reconnects only if we were connected.
"""
# --- Update params, tear down existing connection/transport, then reconnect (or schedule) only if appropriate ---
if uri is not None:
self._set_uri(uri)
if auth is not None:
self._set_auth(auth)

# Remember whether we were connected so we know to disconnect and whether to reconnect at the end
was_already_connected = self.is_connected()

# Prevent on_disconnected (from the disconnect below) from scheduling a reconnect during teardown
self._manual_disconnect = True
if self._reconnect_task and not self._reconnect_task.done():
self._reconnect_task.cancel()
self._reconnect_task = None
if was_already_connected:
await self._internal_disconnect()

# Drop the transport so the next connect() builds a new one with the new uri/auth
self._transport = None
if self._persist and was_already_connected:
# Schedule a single reconnect attempt (after backoff); only if we were connected (no auto-connect on param set)
await self._schedule_reconnect()
elif was_already_connected:
# Non-persist: only reconnect if we had been connected (same as connect() semantics)
await self._internal_connect()

# We're done; clear so future disconnects (e.g. drop) can trigger reconnect if persist
self._manual_disconnect = False
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Exception handling leaves _manual_disconnect in inconsistent state.

If _internal_connect() on line 421 raises, line 424 never executes and _manual_disconnect remains True. While this only affects non-persist mode (where auto-reconnect doesn't apply), it could cause confusion during debugging or if persist mode is enabled later on the same instance.

Consider wrapping in try/finally:

♻️ Suggested fix
         elif was_already_connected:
             # Non-persist: only reconnect if we had been connected (same as connect() semantics)
-            await self._internal_connect()
-
-        # We're done; clear so future disconnects (e.g. drop) can trigger reconnect if persist
-        self._manual_disconnect = False
+            try:
+                await self._internal_connect()
+            finally:
+                # We're done; clear so future disconnects (e.g. drop) can trigger reconnect if persist
+                self._manual_disconnect = False
+        else:
+            self._manual_disconnect = False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-python/src/rocketride/mixins/connection.py` around lines 386
- 424, The method set_connection_params can leave self._manual_disconnect True
if _internal_connect() or _schedule_reconnect() raises; wrap the post-teardown
reconnect logic (the block after setting self._transport = None that calls
self._schedule_reconnect() or self._internal_connect()) in a try/finally so that
self._manual_disconnect is always reset to False in the finally clause; ensure
the try covers both the persist and non-persist branches (i.e., the await
self._schedule_reconnect() and await self._internal_connect() calls) and does
not swallow exceptions (re-raise after finally).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module:client-python Python SDK and MCP client

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python SDK URI normalization can duplicate task-service path

1 participant