Skip to content

Commit db914fd

Browse files
committed
clean up http path/qs handling
1 parent 69b665f commit db914fd

4 files changed

Lines changed: 81 additions & 13 deletions

File tree

src/js/packages/@reactpy/client/src/mount.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,9 @@ export function mountReactPy(props: MountProps) {
1212
);
1313

1414
// Embed the initial HTTP path into the WebSocket URL
15-
componentUrl.searchParams.append("http_pathname", window.location.pathname);
15+
componentUrl.searchParams.append("path", window.location.pathname);
1616
if (window.location.search) {
17-
componentUrl.searchParams.append(
18-
"http_query_string",
19-
window.location.search,
20-
);
17+
componentUrl.searchParams.append("qs", window.location.search);
2118
}
2219

2320
// Configure a new ReactPy client

src/js/packages/@reactpy/client/src/websocket.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import type { CreateReconnectingWebSocketProps } from "./types";
22
import log from "./logger";
33

4+
function syncBrowserLocation(url: URL): void {
5+
// The window will always have a HTTP path, so ReactPy should always be aware of it.
6+
url.searchParams.set("path", window.location.pathname);
7+
8+
if (window.location.search) {
9+
// Set the query string parameter if the HTTP location has a query string.
10+
url.searchParams.set("qs", window.location.search);
11+
} else {
12+
// Remove any existing (potentially stale) query string parameter if the current location doesn't have one
13+
url.searchParams.delete("qs");
14+
}
15+
}
16+
417
export function createReconnectingWebSocket(
518
props: CreateReconnectingWebSocketProps,
619
) {
@@ -15,8 +28,7 @@ export function createReconnectingWebSocket(
1528
if (closed) {
1629
return;
1730
}
18-
props.url.searchParams.set("path", window.location.pathname);
19-
props.url.searchParams.set("qs", window.location.search);
31+
syncBrowserLocation(props.url);
2032
socket.current = new WebSocket(props.url);
2133
socket.current.onopen = () => {
2234
everConnected = true;

src/reactpy/executors/asgi/middleware.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@
4242
_logger = logging.getLogger(__name__)
4343

4444

45+
def _location_from_websocket_query_string(query_string: str) -> Location:
46+
ws_query_string = urllib.parse.parse_qs(query_string, strict_parsing=True)
47+
return Location(
48+
path=ws_query_string.get("path", [""])[0],
49+
query_string=ws_query_string.get("qs", [""])[0],
50+
)
51+
52+
4553
class ReactPyMiddleware:
4654
root_component: RootComponentConstructor | None = None
4755
root_components: dict[str, RootComponentConstructor]
@@ -221,14 +229,10 @@ async def run_dispatcher(self) -> None:
221229
raise RuntimeError("No root component provided.")
222230

223231
# Create a connection object by analyzing the websocket's query string.
224-
ws_query_string = urllib.parse.parse_qs(
225-
self.scope["query_string"].decode(), strict_parsing=True
226-
)
227232
connection = Connection(
228233
scope=self.scope, # type: ignore
229-
location=Location(
230-
path=ws_query_string.get("http_pathname", [""])[0],
231-
query_string=ws_query_string.get("http_query_string", [""])[0],
234+
location=_location_from_websocket_query_string(
235+
self.scope["query_string"].decode()
232236
),
233237
carrier=self,
234238
)

tests/test_asgi/test_standalone.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import reactpy
1010
from reactpy import html
1111
from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT
12+
from reactpy.executors.asgi.middleware import _location_from_websocket_query_string
1213
from reactpy.executors.asgi.standalone import ReactPy
1314
from reactpy.testing import BackendFixture, DisplayFixture, poll
1415
from reactpy.types import Connection, Location
@@ -107,6 +108,60 @@ def ShowRoute():
107108
await poll_location.until_equals(loc)
108109

109110

111+
async def test_use_location_after_reconnect_from_client_navigation(
112+
display: DisplayFixture,
113+
):
114+
location = reactpy.Ref()
115+
116+
@poll
117+
async def poll_location():
118+
return getattr(location, "current", None)
119+
120+
@reactpy.component
121+
def ShowRoute():
122+
location.current = reactpy.use_location()
123+
return html.pre(str(location.current))
124+
125+
await display.page.add_init_script(
126+
"""
127+
(() => {
128+
window.__reactpySockets = [];
129+
const NativeWebSocket = window.WebSocket;
130+
window.WebSocket = class extends NativeWebSocket {
131+
constructor(url, protocols) {
132+
super(url, protocols);
133+
window.__reactpySockets.push(this);
134+
}
135+
};
136+
})();
137+
"""
138+
)
139+
140+
await display.show(ShowRoute)
141+
await poll_location.until_equals(Location("/", ""))
142+
143+
await display.page.evaluate(
144+
"""
145+
() => {
146+
history.pushState({}, "", "/client-route?view=next");
147+
const socket = window.__reactpySockets.at(-1);
148+
if (!socket) {
149+
throw new Error("Missing ReactPy websocket");
150+
}
151+
socket.close();
152+
}
153+
"""
154+
)
155+
156+
await poll_location.until_equals(Location("/client-route", "?view=next"))
157+
158+
159+
def test_location_from_websocket_query_string_uses_path_and_qs():
160+
assert _location_from_websocket_query_string(
161+
"path=%2Fcurrent&qs=%3Fview%3Dnext"
162+
) == Location("/current", "?view=next")
163+
164+
110165
async def test_carrier(display: DisplayFixture):
111166
hook_val = reactpy.Ref()
112167

0 commit comments

Comments
 (0)