Skip to content

Commit c7f9647

Browse files
committed
Add safety guards to navigation context stack
1 parent 33c2c81 commit c7f9647

File tree

3 files changed

+18
-7
lines changed

3 files changed

+18
-7
lines changed

packages/react/src/reactrouter-compat-utils/instrumentation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ function wrapPatchRoutesOnNavigation(
752752
try {
753753
result = await originalPatchRoutes(args);
754754
} finally {
755-
clearNavigationContext();
755+
clearNavigationContext(activeRootSpan);
756756
}
757757

758758
const currentActiveRootSpan = getActiveRootSpan();

packages/react/src/reactrouter-compat-utils/lazy-routes.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import { getActiveRootSpan, getNavigationContext } from './utils';
1111
*/
1212
function captureCurrentLocation(): Location | null {
1313
const navContext = getNavigationContext();
14-
if (navContext) {
14+
// Only use navigation context if targetPath is defined (it can be undefined
15+
// if patchRoutesOnNavigation was invoked without a path argument)
16+
if (navContext?.targetPath) {
1517
return {
1618
pathname: navContext.targetPath,
1719
search: '',

packages/react/src/reactrouter-compat-utils/utils.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,29 @@ let _stripBasename: boolean = false;
1010
// Required because window.location hasn't updated yet when handlers are invoked.
1111
// Uses a stack to handle overlapping navigations correctly (LIFO semantics).
1212
interface NavigationContext {
13-
targetPath: string;
13+
targetPath: string | undefined;
1414
span: Span | undefined;
1515
}
1616

1717
const _navigationContextStack: NavigationContext[] = [];
18+
const MAX_CONTEXT_STACK_SIZE = 10;
1819

1920
/** Pushes a navigation context before invoking patchRoutesOnNavigation. */
20-
export function setNavigationContext(targetPath: string, span: Span | undefined): void {
21+
export function setNavigationContext(targetPath: string | undefined, span: Span | undefined): void {
22+
// Prevent unbounded stack growth from cleanup failures or rapid navigations
23+
if (_navigationContextStack.length >= MAX_CONTEXT_STACK_SIZE) {
24+
_navigationContextStack.shift(); // Remove oldest
25+
}
2126
_navigationContextStack.push({ targetPath, span });
2227
}
2328

24-
/** Pops the most recent navigation context after patchRoutesOnNavigation completes. */
25-
export function clearNavigationContext(): void {
26-
_navigationContextStack.pop();
29+
/** Pops the navigation context for the given span after patchRoutesOnNavigation completes. */
30+
export function clearNavigationContext(span: Span | undefined): void {
31+
// Only pop if top of stack matches this span to prevent corruption from mismatched calls
32+
const top = _navigationContextStack[_navigationContextStack.length - 1];
33+
if (top?.span === span) {
34+
_navigationContextStack.pop();
35+
}
2736
}
2837

2938
/** Gets the current (most recent) navigation context if inside a patchRoutesOnNavigation call. */

0 commit comments

Comments
 (0)