Skip to content

Fix medium-severity audit findings across router, hooks, and matcher#2

Merged
ConsoleTVs merged 2 commits into
mainfrom
fix/medium-severity-audit-findings
Apr 12, 2026
Merged

Fix medium-severity audit findings across router, hooks, and matcher#2
ConsoleTVs merged 2 commits into
mainfrom
fix/medium-severity-audit-findings

Conversation

@ConsoleTVs
Copy link
Copy Markdown
Member

Summary

React Hooks

  • Fix useSearchParams subscription tearing by introducing UrlContext — search params now derive from React state instead of the mutable navigation.currentEntry, preventing concurrent-mode tearing
  • Fix setSearchParams silently dropping the URL hash fragment when updating search parameters
  • Fix canGoBack/canGoForward not being reactive — useBack and useForward now subscribe to the currententrychange event and keep their boolean in React state

Route Builder (createRouter)

  • Add static redirect cycle detection — self-redirects and multi-hop loops throw at registration time
  • Guard route builder against reuse after terminal methods (.render(), .redirect(), .group()) — calling any method on a consumed builder now throws a descriptive error
  • Export new UrlContext from the barrel

Matcher

  • Throw on duplicate route registration instead of silently overwriting — prevents accidental route shadowing
  • Add per-matcher prefetch deduplication via WeakMap<Matcher, Set<string>> cache to prevent thundering-herd when many <Link> components point to the same destination

Type Safety

  • Fix NavigationContext typed as Context<Navigation> (using null as unknown as Navigation) but actually defaulting to null — now correctly typed as Context<Navigation | null>

Tests

  • 26 new tests (195 → 221), all passing
  • New test suites: redirect cycle detection, builder consumed guard, duplicate route registration, hash preservation, null UrlContext fallback

Breaking Changes

NavigationContext type changed to Context<Navigation | null>

What changed: The context default is now properly typed as nullable instead of using null as unknown as Navigation.

Migration: Code using use(NavigationContext) directly must handle the null case. The useNavigation() hook already throws a descriptive error on null, so hook consumers are unaffected.

Why: The previous typing masked a runtime null value, causing potential crashes when consumed outside a Router.

matcher.register() throws on duplicate patterns

What changed: Registering the same path pattern twice now throws instead of silently overwriting.

Migration: Remove duplicate route registrations. If intentional overwriting is needed, create a new matcher.

Why: Silent overwriting caused hard-to-debug routing bugs where the last registration would win.

Route builder throws on reuse after terminal methods

What changed: Calling .middleware(), .scroll(), etc. after .render()/.redirect()/.group() now throws.

Migration: Create a new builder via route('/path') for each route instead of reusing a consumed builder.

Why: Reusing a consumed builder led to confusing behavior where configuration appeared to be set but was silently ignored.

… matcher

- Fix useSearchParams subscription tearing by reading from UrlContext
  (React state) instead of mutable navigation.currentEntry
- Fix setSearchParams dropping hash fragment by appending url.hash
- Fix canGoBack/canGoForward not reactive by subscribing to
  currententrychange event in useBack and useForward hooks
- Add redirect cycle detection in createRouter for static targets
- Guard route builder against reuse after terminal methods
- Throw on duplicate route registration in matcher
- Add prefetch deduplication via per-matcher WeakMap cache
- Fix NavigationContext typed as non-null but defaulting to null
- Export UrlContext from barrel

BREAKING CHANGE: NavigationContext now typed as Context<Navigation | null>
instead of Context<Navigation>. Code using use(NavigationContext)
directly must handle the null case. The useNavigation() hook already
throws a descriptive error on null, so hook consumers are unaffected.

BREAKING CHANGE: matcher.register() now throws on duplicate pattern
registration instead of silently overwriting. This prevents accidental
route shadowing that previously caused hard-to-debug routing bugs.

BREAKING CHANGE: RouteBuilder methods now throw when called after a
terminal method (.render(), .redirect(), .group()). Previously the
builder could be silently reused, leading to confusing behavior.
@ConsoleTVs ConsoleTVs marked this pull request as ready for review April 12, 2026 12:23
@ConsoleTVs ConsoleTVs merged commit 62d9a9c into main Apr 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant