Repository: microsoft/playwright-python
Pull request #2687 (2024-12-12 22:09:11)

fix(webSocketRoute): allow no trailing slash in route matching
https://github.com/microsoft/playwright-python/issues/2686

mxschmitt: > Why does it work in Node.js and other ports?

In Node.js we do the same here: https://github.com/microsoft/playwright/blob/16a1552e7490139f295086ab52cc0d7b3901eaac/packages/playwright-core/src/utils/isomorphic/urlMatch.ts#L128

The `URL` constructor does normalise the URL and add a trailing slash.

Patch:
diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py
index d0737be..538d553 100644
--- a/playwright/_impl/_helper.py
+++ b/playwright/_impl/_helper.py
@@ -34,7 +34,7 @@ from typing import (
     Union,
     cast,
 )
-from urllib.parse import urljoin
+from urllib.parse import urljoin, urlparse
 
 from playwright._impl._api_structures import NameValue
 from playwright._impl._errors import (
@@ -157,6 +157,10 @@ def url_matches(
             base_url = re.sub(r"^http", "ws", base_url)
         if base_url:
             match = urljoin(base_url, match)
+        parsed = urlparse(match)
+        if parsed.path == "":
+            parsed = parsed._replace(path="/")
+            match = parsed.geturl()
     if isinstance(match, str):
         match = glob_to_regex(match)
     if isinstance(match, Pattern):


Test patch:
diff --git a/tests/async/test_route_web_socket.py b/tests/async/test_route_web_socket.py
index 2ebda4b..465832a 100644
--- a/tests/async/test_route_web_socket.py
+++ b/tests/async/test_route_web_socket.py
@@ -346,3 +346,36 @@ async def test_should_work_with_base_url(browser: Browser, server: Server) -> No
             f"message: data=echo origin=ws://localhost:{server.PORT} lastEventId=",
         ],
     )
+
+
+async def test_should_work_with_no_trailing_slash(page: Page, server: Server) -> None:
+    log: list[str] = []
+
+    async def handle_ws(ws: WebSocketRoute) -> None:
+        def on_message(message: Union[str, bytes]) -> None:
+            assert isinstance(message, str)
+            log.append(message)
+            ws.send("response")
+
+        ws.on_message(on_message)
+
+    # No trailing slash in the route pattern
+    await page.route_web_socket(f"ws://localhost:{server.PORT}", handle_ws)
+
+    await page.goto("about:blank")
+    await page.evaluate(
+        """({ port }) => {
+            window.log = [];
+            // No trailing slash in WebSocket URL
+            window.ws = new WebSocket('ws://localhost:' + port);
+            window.ws.addEventListener('message', event => window.log.push(event.data));
+        }""",
+        {"port": server.PORT},
+    )
+
+    await assert_equal(
+        lambda: page.evaluate("window.ws.readyState"), 1  # WebSocket.OPEN
+    )
+    await page.evaluate("window.ws.send('query')")
+    await assert_equal(lambda: log, ["query"])
+    await assert_equal(lambda: page.evaluate("window.log"), ["response"])
diff --git a/tests/sync/test_route_web_socket.py b/tests/sync/test_route_web_socket.py
index a22a6e8..2e97ebd 100644
--- a/tests/sync/test_route_web_socket.py
+++ b/tests/sync/test_route_web_socket.py
@@ -340,3 +340,34 @@ def test_should_work_with_base_url(browser: Browser, server: Server) -> None:
             f"message: data=echo origin=ws://localhost:{server.PORT} lastEventId=",
         ],
     )
+
+
+def test_should_work_with_no_trailing_slash(page: Page, server: Server) -> None:
+    log: list[str] = []
+
+    async def handle_ws(ws: WebSocketRoute) -> None:
+        def on_message(message: Union[str, bytes]) -> None:
+            assert isinstance(message, str)
+            log.append(message)
+            ws.send("response")
+
+        ws.on_message(on_message)
+
+    # No trailing slash in the route pattern
+    page.route_web_socket(f"ws://localhost:{server.PORT}", handle_ws)
+
+    page.goto("about:blank")
+    page.evaluate(
+        """({ port }) => {
+            window.log = [];
+            // No trailing slash in WebSocket URL
+            window.ws = new WebSocket('ws://localhost:' + port);
+            window.ws.addEventListener('message', event => window.log.push(event.data));
+        }""",
+        {"port": server.PORT},
+    )
+
+    assert_equal(lambda: page.evaluate("window.ws.readyState"), 1)  # WebSocket.OPEN
+    page.evaluate("window.ws.send('query')")
+    assert_equal(lambda: log, ["query"])
+    assert_equal(lambda: page.evaluate("window.log"), ["response"])