Skip to content

Commit 54f9bdc

Browse files
authored
Merge pull request #285 from autoscrape-labs/fix/websocket-auth
Fix websocket reconnection athentication loss
2 parents c8f6f5d + c6474a0 commit 54f9bdc

File tree

3 files changed

+36
-5
lines changed

3 files changed

+36
-5
lines changed

pydoll/browser/chromium/base.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -821,14 +821,21 @@ def _get_tab_kwargs(self, target_id: str, browser_context_id: Optional[str] = No
821821

822822
def _get_tab_ws_address(self, tab_id: str) -> str:
823823
"""
824-
Get WebSocket address for tab. If tab_id is not provided,
825-
it will be derived from the targets.
824+
Get WebSocket address for a specific tab, preserving any query or fragment
825+
components present in the original browser-level WebSocket URL.
826+
827+
This ensures authentication tokens passed via query string (e.g.,
828+
ws://host/devtools/browser/abc?token=XYZ) are retained when switching
829+
to the page-level endpoint (devtools/page/<tab_id>), which is critical
830+
for providers like Browserless or authenticated CDP proxies.
826831
"""
827832
if not self._ws_address:
828833
raise InvalidWebSocketAddress('WebSocket address is not set')
829834

830-
ws_domain = '/'.join(self._ws_address.split('/')[:3])
831-
return f'{ws_domain}/devtools/page/{tab_id}'
835+
parts = urlsplit(self._ws_address)
836+
# Preserve scheme and netloc; build the page path and keep query/fragment
837+
page_path = f'/devtools/page/{tab_id}'
838+
return urlunsplit((parts.scheme, parts.netloc, page_path, parts.query, parts.fragment))
832839

833840
@staticmethod
834841
def _sanitize_proxy_and_extract_auth(

pydoll/browser/managers/browser_process_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def start_browser_process(
5555
@staticmethod
5656
def _default_process_creator(command: list[str]) -> subprocess.Popen:
5757
"""Create browser process with output capture to prevent console clutter."""
58-
return subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
58+
return subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
5959

6060
def stop_process(self):
6161
"""

tests/test_browser/test_browser_base.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,24 @@ async def test_connect_with_ws_address_returns_tab_and_sets_handler_ws(mock_brow
219219
assert tab._ws_address == 'ws://localhost:9222/devtools/page/p1'
220220

221221

222+
@pytest.mark.asyncio
223+
async def test_connect_with_ws_address_preserves_token_in_tab_ws(mock_browser):
224+
ws_browser = 'ws://localhost:9222/devtools/browser/abcdef?token=secrettoken'
225+
mock_browser.get_targets = AsyncMock(return_value=[{'type': 'page', 'url': 'https://example', 'targetId': 'p1'}])
226+
mock_browser._get_valid_tab_id = AsyncMock(return_value='p1')
227+
mock_browser._connection_handler._ensure_active_connection = AsyncMock()
228+
229+
tab = await mock_browser.connect(ws_browser)
230+
231+
assert mock_browser._ws_address == ws_browser
232+
assert mock_browser._connection_handler._ws_address == ws_browser
233+
mock_browser._connection_handler._ensure_active_connection.assert_awaited_once()
234+
235+
# Token should be preserved in page-level ws URL
236+
assert isinstance(tab, Tab)
237+
assert tab._ws_address == 'ws://localhost:9222/devtools/page/p1?token=secrettoken'
238+
239+
222240
@pytest.mark.asyncio
223241
async def test_new_tab_uses_ws_base_when_ws_address_present(mock_browser):
224242
# Simulate browser connected via ws
@@ -365,6 +383,12 @@ def test__get_tab_ws_address_raises_when_ws_not_set(mock_browser):
365383
mock_browser._get_tab_ws_address('some-tab')
366384

367385

386+
def test__get_tab_ws_address_preserves_query_and_fragment(mock_browser):
387+
mock_browser._ws_address = 'ws://host:9222/devtools/browser/abc?token=XYZ#frag'
388+
result = mock_browser._get_tab_ws_address('tab1')
389+
assert result == 'ws://host:9222/devtools/page/tab1?token=XYZ#frag'
390+
391+
368392
@pytest.mark.asyncio
369393
async def test_get_window_id(mock_browser):
370394
mock_browser.get_targets = AsyncMock(return_value=[{'targetId': 'target1', 'type': 'page'}])

0 commit comments

Comments
 (0)