Skip to content

Commit

Permalink
Issue-1473: App Crash with Invalid scriptURL (#1476)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
morganick committed May 15, 2024
1 parent 69631c6 commit d45ee2a
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 7 deletions.
71 changes: 71 additions & 0 deletions lib/reactotron-react-native/src/helpers/parseURL.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
19 changes: 19 additions & 0 deletions lib/reactotron-react-native/src/helpers/parseURL.ts
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 13 additions & 7 deletions lib/reactotron-react-native/src/reactotron-react-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {}

Expand All @@ -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<ReactotronReactNative> = {
createSocket: (path: string) => new WebSocket(path), // eslint-disable-line
Expand Down
3 changes: 3 additions & 0 deletions scripts/reset.sh
Original file line number Diff line number Diff line change
@@ -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"
Expand Down

0 comments on commit d45ee2a

Please sign in to comment.