-
Notifications
You must be signed in to change notification settings - Fork 626
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support ';&' as an alternative to '?' in bundle URLs
Summary: Recent versions of JavaScriptCore strip query strings from `Error.prototype.stack`, which breaks our use of query strings to carry bundle build parameters. See facebook/react-native#36794 for context. This allows Metro to accept an alternative format where we interpret the special reserved character sequence `;&`, the first time it appears in a URL *path*, as equivalent to `?`. So that this does not break custom `rewriteRequestUrl` implementations, we (temporarily) pass a normalised URL (`;&` replaced with `?`), and then reverse that operation on the URL returned by `rewriteRequestUrl`. *[Simplifying the details here somewhat - see the implementation for specifics].* Changelog: ``` **[Feature]**: Support alternative JavaScriptCore-safe URL format (`;&` as `?`). ``` Differential Revision: D45477505 fbshipit-source-id: ff78c751d45fcf2f7508cce9fcb6f3a81f58ee32
- Loading branch information
1 parent
97d5544
commit fa99364
Showing
3 changed files
with
191 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @flow strict-local | ||
* @format | ||
* @oncall react_native | ||
*/ | ||
|
||
const {normalizeJscUrl, toJscUrl} = require('../jscUrlUtils'); | ||
|
||
describe('normalizeJscUrl', () => { | ||
test.each([ | ||
[ | ||
'/path1/path2;&foo=bar?bar=baz#frag?', | ||
'/path1/path2?foo=bar&bar=baz#frag?', | ||
], | ||
[ | ||
'relative/path;&foo=bar?bar=baz#frag?', | ||
'relative/path?foo=bar&bar=baz#frag?', | ||
], | ||
[ | ||
'https://user;&:password;&@mydomain.com:8080/path1/path2;&foo=bar?bar=baz#frag?', | ||
'https://user%3B&:password%3B&@mydomain.com:8080/path1/path2?foo=bar&bar=baz#frag?', | ||
], | ||
[ | ||
'http://127.0.0.1/path1/path2;&foo=bar&bar=baz', | ||
'http://127.0.0.1/path1/path2?foo=bar&bar=baz', | ||
], | ||
])('rewrites urls treating ;& in paths as ? (%s => %s)', (input, output) => { | ||
expect(normalizeJscUrl(input)).toEqual(output); | ||
}); | ||
|
||
test.each([ | ||
['http://user;&:password;&@mydomain.com/foo?bar=zoo?baz=quux;&'], | ||
['/foo?bar=zoo?baz=quux'], | ||
['proto:arbitrary_bad_url'], | ||
['*'], | ||
['relative/path'], | ||
])('returns other strings exactly as given (%s)', input => { | ||
expect(normalizeJscUrl(input)).toEqual(input); | ||
}); | ||
}); | ||
|
||
describe('toJscUrl', () => { | ||
test.each([ | ||
[ | ||
'https://user;&:password;&@mydomain.com:8080/path1/path2?foo=bar&bar=question?#frag?', | ||
'https://user%3B&:password%3B&@mydomain.com:8080/path1/path2;&foo=bar&bar=question%3F#frag?', | ||
], | ||
[ | ||
'http://127.0.0.1/path1/path2?foo=bar', | ||
'http://127.0.0.1/path1/path2;&foo=bar', | ||
], | ||
['*', '*'], | ||
['/absolute/path', '/absolute/path'], | ||
['relative/path', 'relative/path'], | ||
['http://127.0.0.1/path1/path', 'http://127.0.0.1/path1/path'], | ||
[ | ||
'/path1/path2?foo=bar&bar=question?#frag?', | ||
'/path1/path2;&foo=bar&bar=question%3F#frag?', | ||
], | ||
[ | ||
'relative/path?foo=bar&bar=question?#frag?', | ||
'relative/path;&foo=bar&bar=question%3F#frag?', | ||
], | ||
])( | ||
'replaces the first ? with a JSC-friendly delimeter, url-encodes subsequent ? (%s => %s)', | ||
(input, output) => { | ||
expect(toJscUrl(input)).toEqual(output); | ||
}, | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @format | ||
* @flow strict | ||
*/ | ||
|
||
/** | ||
* These functions are for handling of query-string free URLs, necessitated | ||
* by query string stripping of URLs in JavaScriptCore stack traces | ||
* introduced in iOS 16.4. | ||
* | ||
* See https://github.com/facebook/react-native/issues/36794 for context. | ||
*/ | ||
|
||
const PLACEHOLDER_HOST = 'placeholder://example.com'; | ||
const JSC_QUERY_STRING_DELIMETER = ';&'; | ||
|
||
function normalizeJscUrl(urlToNormalize: string): string { | ||
try { | ||
const urlObj = new URL(urlToNormalize, PLACEHOLDER_HOST); | ||
const delimeterIdx = urlObj.pathname.indexOf(JSC_QUERY_STRING_DELIMETER); | ||
if (delimeterIdx === -1) { | ||
return urlToNormalize; | ||
} | ||
|
||
// HTTP request lines may be either absolute *paths* (HTTP GET /foo) or | ||
// absolute URIs (HTTP GET http://domain.com/foo) - so we should handle | ||
// both. | ||
// ( https://datatracker.ietf.org/doc/html/rfc9112#name-request-target ) | ||
const isAbsoluteURI = !urlObj.href.startsWith(PLACEHOLDER_HOST); | ||
|
||
// Relative paths are not valid in an HTTP GET request line, but account | ||
// for them for completeness. We'll use this to conditionally remove the | ||
// `/` added by `URL`. | ||
const isAbsolutePath = urlToNormalize.startsWith('/'); | ||
|
||
// This is our regular pathname | ||
const pathBeforeDelimeter = urlObj.pathname.substring(0, delimeterIdx); | ||
// This will become our query string | ||
const pathAfterDelimeter = urlObj.pathname.substring( | ||
delimeterIdx + JSC_QUERY_STRING_DELIMETER.length, | ||
); | ||
|
||
urlObj.pathname = pathBeforeDelimeter; | ||
if (urlObj.search) { | ||
// JSC-style URLs wouldn't normally be expected to have regular query | ||
// strings, but append them if present | ||
urlObj.search = `?${pathAfterDelimeter}&${urlObj.search.substring(1)}`; | ||
} else { | ||
urlObj.search = `?${pathAfterDelimeter}`; | ||
} | ||
let urlToReturn = urlObj.href; | ||
if (!isAbsoluteURI) { | ||
urlToReturn = urlToReturn.replace(PLACEHOLDER_HOST, ''); | ||
if (!isAbsolutePath) { | ||
urlToReturn = urlToReturn.substring(1); | ||
} | ||
} | ||
return urlToReturn; | ||
} catch (e) { | ||
// Preserve malformed URLs | ||
return urlToNormalize; | ||
} | ||
} | ||
|
||
function toJscUrl(urlToConvert: string): string { | ||
try { | ||
const urlObj = new URL(urlToConvert, PLACEHOLDER_HOST); | ||
if (urlObj.search == null || !urlObj.search.startsWith('?')) { | ||
return urlToConvert; | ||
} | ||
const isAbsoluteURI = !urlObj.href.startsWith(PLACEHOLDER_HOST); | ||
// Relative paths are not valid in an HTTP GET request line, but may appear otherwise | ||
const isAbsolutePath = urlToConvert.startsWith('/'); | ||
|
||
const queryString = urlObj.search.substring(1); | ||
// NB: queryString may legally contain unencoded '?' in key or value names. | ||
// Writing them into the path will implicitly encode them. | ||
urlObj.pathname = | ||
urlObj.pathname + JSC_QUERY_STRING_DELIMETER + queryString; | ||
urlObj.search = ''; | ||
let urlToReturn = urlObj.href; | ||
if (!isAbsoluteURI) { | ||
urlToReturn = urlToReturn.replace(PLACEHOLDER_HOST, ''); | ||
if (!isAbsolutePath) { | ||
urlToReturn = urlToReturn.substring(1); | ||
} | ||
} | ||
return urlToReturn; | ||
} catch (e) { | ||
// Preserve malformed URLs | ||
return urlToConvert; | ||
} | ||
} | ||
|
||
module.exports = { | ||
normalizeJscUrl, | ||
toJscUrl, | ||
}; |