Skip to content

Commit d45ee2a

Browse files
authored
Issue-1473: App Crash with Invalid scriptURL (#1476)
## Please verify the following: - [x] `yarn build-and-test:local` passes - [x] I have added tests for any new features, if relevant - [ ] `README.md` (or relevant documentation) has been updated with your changes ## Describe your PR Fixes #1473 where calling split on an invalid string throws a `TypeError`. In the event that the following line does not provide us with a valid URL: https://github.com/facebook/react-native/blob/b38f80aeb6ad986c64fd03f53b2e01a7990e1533/packages/react-native/React/CoreModules/RCTSourceCode.mm#L38 We should catch this and warn the user that we are falling back to the default host: ``` WARN getHost: "Invalid URL: null" for scriptURL - Falling back to localhost ``` ## How to Reproduce I was unable to reproduce this issue locally with my devices as it would not crash the application, but just throw the error. Due to the circumstances of the original issue and that applications configuration, this should allow that application to continue to boot. ## Additional Notes It is still possible to configure Reactotron with an invalid host. This will equally throw an error of `invalid url`. I'd like to create a new PR that allows the application to continue to function, keeps Reactotron is a disconnected state, and notifies the user.
1 parent 69631c6 commit d45ee2a

File tree

4 files changed

+106
-7
lines changed

4 files changed

+106
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { getHostFromUrl } from "./parseURL"
2+
3+
describe("getHostFromUrl", () => {
4+
it("should throw when no host is found", () => {
5+
expect(() => {
6+
getHostFromUrl("")
7+
}).toThrow()
8+
})
9+
10+
it("should get host from URL without scheme", () => {
11+
Object.entries({
12+
localhost: "localhost",
13+
"127.0.0.1": "127.0.0.1",
14+
"[::1]": "[::1]",
15+
}).forEach(([host, url]) => {
16+
expect(getHostFromUrl(url)).toEqual(host)
17+
})
18+
expect(getHostFromUrl("localhost")).toEqual("localhost")
19+
expect(getHostFromUrl("127.0.0.1")).toEqual("127.0.0.1")
20+
})
21+
22+
it("should get the host from URL with http scheme", () => {
23+
Object.entries({
24+
localhost: "http://localhost",
25+
"example.com": "http://example.com",
26+
}).forEach(([host, url]) => {
27+
expect(getHostFromUrl(url)).toEqual(host)
28+
})
29+
})
30+
31+
it("should get the host from URL with https scheme", () => {
32+
Object.entries({
33+
localhost: "https://localhost",
34+
"example.com": "https://example.com",
35+
}).forEach(([host, url]) => {
36+
expect(getHostFromUrl(url)).toEqual(host)
37+
})
38+
})
39+
40+
it("should get the host from URL and ignore path, port, and query params", () => {
41+
Object.entries({
42+
localhost:
43+
"http://localhost:8081/.expo/.virtual-metro-entry.bundle?platform=ios&dev=true&lazy=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&app=com.reactotronapp",
44+
"192.168.1.141":
45+
"https://192.168.1.141:8081/.expo/.virtual-metro-entry.bundle?platform=ios&dev=true&lazy=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&app=com.reactotronapp",
46+
}).forEach(([host, url]) => {
47+
expect(getHostFromUrl(url)).toEqual(host)
48+
})
49+
})
50+
51+
it("should get the host from an IPv6 URL and ignore path, port, and query params", () => {
52+
Object.entries({
53+
"[::1]":
54+
"http://[::1]:8081/.expo/.virtual-metro-entry.bundle?platform=ios&dev=true&lazy=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&app=com.reactotronapp",
55+
"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]":
56+
"https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8081/.expo/.virtual-metro-entry.bundle?platform=ios&dev=true&lazy=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&app=com.reactotronapp",
57+
}).forEach(([host, url]) => {
58+
expect(getHostFromUrl(url)).toEqual(host)
59+
})
60+
})
61+
62+
it("should get the host from URL with hyphens", () => {
63+
expect(getHostFromUrl("https://example-app.com")).toEqual("example-app.com")
64+
})
65+
66+
it("should throw when the URL is an unsupported scheme", () => {
67+
expect(() => {
68+
getHostFromUrl("file:///Users/tron")
69+
}).toThrow()
70+
})
71+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Given a valid http(s) URL, the host for the given URL
3+
* is returned.
4+
*
5+
* @param url {string} URL to extract the host from
6+
* @returns {string} host of given URL or throws
7+
*/
8+
// Using a capture group to extract the hostname from a URL
9+
export function getHostFromUrl(url: string) {
10+
// Group 1: http(s)://
11+
// Group 2: host
12+
// Group 3: port
13+
// Group 4: rest
14+
const host = url.match(/^(?:https?:\/\/)?(\[[^\]]+\]|[^/:\s]+)(?::\d+)?(?:[/?#]|$)/)?.[1]
15+
16+
if (typeof host !== "string") throw new Error("Invalid URL - host not found")
17+
18+
return host
19+
}

lib/reactotron-react-native/src/reactotron-react-native.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import networking, { NetworkingOptions } from "./plugins/networking"
1919
import storybook from "./plugins/storybook"
2020
import devTools from "./plugins/devTools"
2121
import trackGlobalLogs from "./plugins/trackGlobalLogs"
22+
import { getHostFromUrl } from "./helpers/parseURL"
2223

2324
const constants = NativeModules.PlatformConstants || {}
2425

@@ -33,13 +34,18 @@ let tempClientId: string | null = null
3334
*
3435
* On an Android emulator, if you want to connect any servers of local, you will need run adb reverse on your terminal. This function gets the localhost IP of host machine directly to bypass this.
3536
*/
36-
const getHost = (defaultHost = "localhost") =>
37-
typeof NativeModules?.SourceCode?.getConstants().scriptURL === "string" // type guard in case this ever breaks https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/NativeModules/specs/NativeSourceCode.js#L15-L21
38-
? NativeModules.SourceCode.scriptURL // Example: 'http://192.168.0.100:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=com.helloworld'
39-
.split("://")[1] // Remove the scheme: '192.168.0.100:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=com.helloworld'
40-
.split("/")[0] // Remove the path: '192.168.0.100:8081'
41-
.split(":")[0] // Remove the port: '192.168.0.100'
42-
: defaultHost
37+
const getHost = (defaultHost = "localhost") => {
38+
try {
39+
// RN Reference: https://github.com/facebook/react-native/blob/main/packages/react-native/src/private/specs/modules/NativeSourceCode.js
40+
const scriptURL = NativeModules?.SourceCode?.getConstants().scriptURL
41+
if (typeof scriptURL !== "string") throw new Error("Invalid non-string URL")
42+
43+
return getHostFromUrl(scriptURL)
44+
} catch (error) {
45+
console.warn(`getHost: "${error.message}" for scriptURL - Falling back to ${defaultHost}`)
46+
return defaultHost
47+
}
48+
}
4349

4450
const DEFAULTS: ClientOptions<ReactotronReactNative> = {
4551
createSocket: (path: string) => new WebSocket(path), // eslint-disable-line

scripts/reset.sh

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#!/bin/bash
22

3+
echo "Nx Reset - Clears all the cached Nx artifacts and metadata about the workspace and shuts down the Nx Daemon."
4+
yarn nx reset
5+
36
sh scripts/clean.sh
47

58
echo "Removing all node_modules folders from the project"

0 commit comments

Comments
 (0)