From d45ee2a72f85d583f05106e8fb412d4bccef3b02 Mon Sep 17 00:00:00 2001 From: Nick Morgan Date: Wed, 15 May 2024 09:09:10 -0400 Subject: [PATCH] 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. --- .../src/helpers/parseURL.test.ts | 71 +++++++++++++++++++ .../src/helpers/parseURL.ts | 19 +++++ .../src/reactotron-react-native.ts | 20 ++++-- scripts/reset.sh | 3 + 4 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 lib/reactotron-react-native/src/helpers/parseURL.test.ts create mode 100644 lib/reactotron-react-native/src/helpers/parseURL.ts diff --git a/lib/reactotron-react-native/src/helpers/parseURL.test.ts b/lib/reactotron-react-native/src/helpers/parseURL.test.ts new file mode 100644 index 000000000..0afa09357 --- /dev/null +++ b/lib/reactotron-react-native/src/helpers/parseURL.test.ts @@ -0,0 +1,71 @@ +import { getHostFromUrl } from "./parseURL" + +describe("getHostFromUrl", () => { + it("should throw when no host is found", () => { + expect(() => { + getHostFromUrl("") + }).toThrow() + }) + + it("should get host from URL without scheme", () => { + Object.entries({ + localhost: "localhost", + "127.0.0.1": "127.0.0.1", + "[::1]": "[::1]", + }).forEach(([host, url]) => { + expect(getHostFromUrl(url)).toEqual(host) + }) + expect(getHostFromUrl("localhost")).toEqual("localhost") + expect(getHostFromUrl("127.0.0.1")).toEqual("127.0.0.1") + }) + + it("should get the host from URL with http scheme", () => { + Object.entries({ + localhost: "http://localhost", + "example.com": "http://example.com", + }).forEach(([host, url]) => { + expect(getHostFromUrl(url)).toEqual(host) + }) + }) + + it("should get the host from URL with https scheme", () => { + Object.entries({ + localhost: "https://localhost", + "example.com": "https://example.com", + }).forEach(([host, url]) => { + expect(getHostFromUrl(url)).toEqual(host) + }) + }) + + it("should get the host from URL and ignore path, port, and query params", () => { + Object.entries({ + localhost: + "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", + "192.168.1.141": + "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", + }).forEach(([host, url]) => { + expect(getHostFromUrl(url)).toEqual(host) + }) + }) + + it("should get the host from an IPv6 URL and ignore path, port, and query params", () => { + Object.entries({ + "[::1]": + "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", + "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]": + "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", + }).forEach(([host, url]) => { + expect(getHostFromUrl(url)).toEqual(host) + }) + }) + + it("should get the host from URL with hyphens", () => { + expect(getHostFromUrl("https://example-app.com")).toEqual("example-app.com") + }) + + it("should throw when the URL is an unsupported scheme", () => { + expect(() => { + getHostFromUrl("file:///Users/tron") + }).toThrow() + }) +}) diff --git a/lib/reactotron-react-native/src/helpers/parseURL.ts b/lib/reactotron-react-native/src/helpers/parseURL.ts new file mode 100644 index 000000000..863adc1e4 --- /dev/null +++ b/lib/reactotron-react-native/src/helpers/parseURL.ts @@ -0,0 +1,19 @@ +/** + * Given a valid http(s) URL, the host for the given URL + * is returned. + * + * @param url {string} URL to extract the host from + * @returns {string} host of given URL or throws + */ +// Using a capture group to extract the hostname from a URL +export function getHostFromUrl(url: string) { + // Group 1: http(s):// + // Group 2: host + // Group 3: port + // Group 4: rest + const host = url.match(/^(?:https?:\/\/)?(\[[^\]]+\]|[^/:\s]+)(?::\d+)?(?:[/?#]|$)/)?.[1] + + if (typeof host !== "string") throw new Error("Invalid URL - host not found") + + return host +} diff --git a/lib/reactotron-react-native/src/reactotron-react-native.ts b/lib/reactotron-react-native/src/reactotron-react-native.ts index 5e333c848..bf80b8119 100644 --- a/lib/reactotron-react-native/src/reactotron-react-native.ts +++ b/lib/reactotron-react-native/src/reactotron-react-native.ts @@ -19,6 +19,7 @@ import networking, { NetworkingOptions } from "./plugins/networking" import storybook from "./plugins/storybook" import devTools from "./plugins/devTools" import trackGlobalLogs from "./plugins/trackGlobalLogs" +import { getHostFromUrl } from "./helpers/parseURL" const constants = NativeModules.PlatformConstants || {} @@ -33,13 +34,18 @@ let tempClientId: string | null = null * * 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. */ -const getHost = (defaultHost = "localhost") => - 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 - ? 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' - .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' - .split("/")[0] // Remove the path: '192.168.0.100:8081' - .split(":")[0] // Remove the port: '192.168.0.100' - : defaultHost +const getHost = (defaultHost = "localhost") => { + try { + // RN Reference: https://github.com/facebook/react-native/blob/main/packages/react-native/src/private/specs/modules/NativeSourceCode.js + const scriptURL = NativeModules?.SourceCode?.getConstants().scriptURL + if (typeof scriptURL !== "string") throw new Error("Invalid non-string URL") + + return getHostFromUrl(scriptURL) + } catch (error) { + console.warn(`getHost: "${error.message}" for scriptURL - Falling back to ${defaultHost}`) + return defaultHost + } +} const DEFAULTS: ClientOptions = { createSocket: (path: string) => new WebSocket(path), // eslint-disable-line diff --git a/scripts/reset.sh b/scripts/reset.sh index 796369505..be19fb3cc 100644 --- a/scripts/reset.sh +++ b/scripts/reset.sh @@ -1,5 +1,8 @@ #!/bin/bash +echo "Nx Reset - Clears all the cached Nx artifacts and metadata about the workspace and shuts down the Nx Daemon." +yarn nx reset + sh scripts/clean.sh echo "Removing all node_modules folders from the project"