-
-
Notifications
You must be signed in to change notification settings - Fork 10.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: fix encoding/matching issues with special chars #9477
Conversation
🦋 Changeset detectedLatest commit: 314888d The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@@ -88,7 +89,8 @@ describe("navigate with params", () => { | |||
let pathname = window.location.pathname.replace(/%20/g, "+"); | |||
expect(pathname).toEqual("/blog/react+router"); | |||
|
|||
expect(node.innerHTML).toMatch(/react router/); | |||
// Note decodeURIComponent doesn't decode + | |||
expect(node.innerHTML).toMatch(/react\+router/); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a false positive test previously - and was seemingly only because we weren't resetting history state between tests. decodeURIComponent
doesn't decode the +
sign
@@ -570,7 +570,7 @@ function getUrlBasedHistory( | |||
} | |||
|
|||
if (v5Compat && listener) { | |||
listener({ action, location }); | |||
listener({ action, location: history.location }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
History should pass the encoded location (from window.location
after pushState
) and not the incoming unencoded location
pathname: url.pathname, | ||
search: url.search, | ||
hash: url.hash, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Encode all incoming router.navigate()
locations the same way window.location
would
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self - move this to a history.encodeLocation
method since we don't need to encode for memory histories
// incoming pathnames are always encoded from either window.location or | ||
// from route.navigate, but we want to match against the unencoded paths | ||
// in the route definitions | ||
safelyDecodeURI(pathname) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decode incoming pathnames for matching against our route definitions
* } | ||
*/ | ||
|
||
let specialChars = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Large set of special character "definitions" of the character, and what we expect it to show up as in location.pathname
, location.search
, and location.hash
since it varies according to the spec: https://url.spec.whatwg.org/#c0-control-percent-encode-set
let routeElements = ( | ||
<> | ||
<Route path="/path" element={<Comp heading="Static Route" />} /> | ||
<Route | ||
path="/inline-param/:slug" | ||
element={<Comp heading="Inline Nested Param Route" />} | ||
/> | ||
<Route path="/param"> | ||
<Route | ||
path=":slug" | ||
element={<Comp heading="Parent Nested Param Route" />} | ||
/> | ||
</Route> | ||
<Route | ||
path="/inline-splat/*" | ||
element={<Comp heading="Inline Nested Splat Route" />} | ||
/> | ||
<Route path="/splat"> | ||
<Route | ||
path="*" | ||
element={<Comp heading="Parent Nested Splat Route" />} | ||
/> | ||
</Route> | ||
<Route | ||
path="/reset" | ||
element={<Link to={navigatePath}>Link to path</Link>} | ||
/> | ||
<Route path="/*" element={<Comp heading="Root Splat Route" />} /> | ||
</> | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test that we can support matching special characters when used in dynamic/splat params in root and nested scenarios
</> | ||
); | ||
|
||
// Render BrowserRouter at the initialized location and confirm we get |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The section here and below basically checks 4 things:
- We match routes properly on initial
BrowserRouter
load (reading fromwindow.location
) - We match routes properly on client-side
BrowserRouter
navigations (throughhistory.push
) - We match routes properly on initial
createBrowserRouter
/RouterProvider
load (reading fromwindow.location
) - We match route properly on client-side
createBrowserRouter
/RouterProvider
navigations (throughrouter.navigate
)
<> | ||
<Route path={`/${char}`} element={<Root />} /> | ||
<Route path="/nested"> | ||
<Route path={char} element={<StaticNested />} /> | ||
</Route> | ||
<Route path="/:param"> | ||
<Route path={char} element={<ParamNested />} /> | ||
</Route> | ||
<Route path="/reset" element={<Link to={path}>Link to path</Link>} /> | ||
<Route path="*" element={<h1>Not Found</h1>} /> | ||
</> | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test that we can define and match against route paths containing special characters
tl;dr;
Replaces #9458 with a fairly comprehensive test suite to ensure that we can handle paths with special characters, match against them, and received properly encoded values from
useLocation
that match whatwindow.location
would report.Background
In 6.3 and prior we were always in a reactive mode to
window.history
so for example, we wouldhistory.pushState
and then call the listeners with the value fromwindow.location
. This flow meant that we inherited the built-in URL encoding performed bywindow.location
. Therefore, if you clicked<Link to="/✅">
you got{ pathname: "/%E2%9C%85", ... }
fromuseLocation()
.In 6.4, we go through the new
@remix-run/router
first and do all of our data fetching andupdateState
beforehistory.pushState
- so we weren't getting the encoding. So at the moment, if you click<Link to="/✅">
you incorrectly get{ pathname: "/✅", ... }
fromuseLocation()
.We also have historically had trouble with defining route paths with special characters since we were matching encoded paths from
window.location
to unencoded paths in our<Route>
objects.This PR fixes these 3 main things surrounding special characters:
history
should always send through thewindow.location
to listeners to ensure full backwards compatibility with 6.3router.navigate
we manually encode the incoming location usingnew URL()
so we get the same encoding behavior aswindow.location
. This ensures that useLocation in .4 matches what it would have returned in a 6.3 app<Route path>
values - allowing us to do<Route path="✅" />