Skip to content

Fixes #31574: stop the stale redirect cookie navigating users away mid-session - #31575

Open
harsh-vador wants to merge 1 commit into
mainfrom
fix/stale-redirect-path-navigation
Open

Fixes #31574: stop the stale redirect cookie navigating users away mid-session#31575
harsh-vador wants to merge 1 commit into
mainfrom
fix/stale-redirect-path-navigation

Conversation

@harsh-vador

Copy link
Copy Markdown
Contributor

Describe your changes:

Fixes #31574

Users browsing the app — most visibly clicking service aggregations / filters in Explore — were intermittently thrown onto the landing page, Glossary or Connections. The click was never the cause.

Root cause. The redirectUrlPath cookie is meant to be a one-shot "resume where you were after login" hint. Today:

  • AuthProvider writes it on every refreshable 401 with a 1-hour TTL, including 401s the silent refresh heals and the user never notices.
  • The axios interceptors are registered once in a useEffect with an empty dep array, so handleStoreProtectedRedirectPath closes over location.pathname from app boot — the stored path is not where the user actually is.
  • PermissionProvider.redirectToStoredPath() runs on every permission fetch and navigates unconditionally. That effect is keyed on currentUser?.teams / ?.rolesarray identities — so every setCurrentUser (persona save, team update, profile edit, boot) replays it.
  • Consumption never deleted the cookie; it re-wrote it with a 1-second expiry, which is a race, not a clear.

So: a token expiry while browsing armed a stale path, and an unrelated user-state update minutes later navigated there. The same cookie also explains "I hit refresh on the upgrade screen and landed somewhere else" — on boot the permission fetch replays an hour-old path and discards the URL that was reloaded.

Changes

  • PermissionProvider.tsx — consume the hint once per session (ref latch, re-armed by resetPermissions() across a logout/login boundary), delete the cookie on read, skip the navigate when the stored path already equals pathname + search, and fix the [history] dep to [navigate].
  • AuthProvider.tsx — read the location at call time (globalThis.location) so the boot-time closure cannot write a stale path, store pathname + search, and store only from resetUserDetails (the paths that really bounce to /signin) instead of from every 401.
  • router.constants.ts / AuthProvider.util.ts — cookie TTL 1 hour → 5 minutes via REDIRECT_PATHNAME_EXPIRY_MS; removed setUrlPathnameExpiryAfterRoute (the 1-second-expiry pseudo-delete).
  • SignUpPage.tsx — a brand-new user has nothing to resume, so the cookie is deleted outright.
  • ErrorBoundary.tsx — retry now re-renders the URL that failed instead of navigate(ROUTES.HOME), and resetKeys on the location clears a stuck boundary on route change.
  • Removed a leftover [VisibilityHandler] console.debug.

Type of change:

  • Bug fix

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • I have added tests to cover my changes.
  • All new and existing tests passed.

Tests

New unit tests (all green, yarn test, 4 suites / 40 tests):

PermissionProvider.test.tsx

  • consumes the stored redirect path once, deletes the cookie, and does not replay it on a second permission fetch triggered by a teams/roles identity change;
  • does not navigate when the stored path is already the current location.

AuthProvider.test.tsx

  • a 401 that the refresh heals writes no redirectUrlPath cookie;
  • a session drop to /signin stores the current path (/explore/tables?quickFilter=abc), not the boot-time one.

Also verified: eslint 0 errors on the changed files, prettier --check clean, tsc --noEmit reports nothing new for them.

Manual test steps

  1. document.cookie = "redirectUrlPath=/glossary; path=/" while sitting on /explore.
  2. Trigger a currentUser update (save a persona preference on My Data) or reload.
  3. Before: yanked to /glossary. After: stays put.
  4. Token-expiry path: shorten the JWT expiry, sit on /explore, let the token expire and keep clicking aggregations — no navigation away.

🤖 Generated with Claude Code

The `redirectUrlPath` cookie was written on every refreshable 401 with a
one-hour TTL, using the pathname captured when the axios interceptors were
registered (once, on mount) rather than the user's current location. It was
then replayed by PermissionProvider on *every* permission fetch — an effect
keyed on `currentUser.teams` / `.roles` array identities, so any
setCurrentUser (persona save, team update, profile edit, boot) re-ran it.
Consumption only re-wrote the cookie with a one-second expiry instead of
deleting it.

Net effect: a token expiry while browsing armed a stale path, and a later
unrelated user-state update navigated there — users clicking around Explore
were dropped on the landing page, Glossary or Connections, and a reload after
the "please refresh" screen discarded the URL they reloaded.

- PermissionProvider: consume the cookie once per session (ref latch re-armed
  by resetPermissions on logout), delete it on read, and skip the navigate
  when the stored path is already the current location.
- AuthProvider: read the location at call time, store `pathname + search`, and
  only store when the session is actually dropped to /signin — not on a 401
  the silent refresh heals.
- Shorten the cookie TTL from 1 hour to 5 minutes and drop
  setUrlPathnameExpiryAfterRoute; SignUpPage now deletes the cookie outright.
- ErrorBoundary: retry re-renders the failing URL instead of navigating to
  HOME, and resetKeys clears a stuck boundary on route change.
- Remove a leftover [VisibilityHandler] console.debug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added the UI UI specific issues label Aug 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@harsh-vador harsh-vador self-assigned this Aug 15, 2026
@harsh-vador harsh-vador added the safe to test Add this label to run secure Github workflows on PRs label Aug 15, 2026
Comment on lines 23 to 37
const ErrorBoundary: React.FC<Props> = ({ children }) => {
const navigate = useNavigate();

const onErrorReset = () => {
navigate(ROUTES.HOME);
};
const location = useLocation();

/*
* Retry renders the URL the user is actually on — sending them to the landing
* page instead silently discarded whatever they were looking at. `resetKeys`
* additionally clears a stuck boundary on any route change, so a failure on
* one page does not swallow the rest of the app.
*/
return (
<ErrorBoundaryWrapper
FallbackComponent={ErrorFallback}
onReset={onErrorReset}>
resetKeys={[location.pathname, location.search]}>
{children}
</ErrorBoundaryWrapper>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: ErrorFallback "Home" button no longer navigates home

The diff removes onReset (which called navigate(ROUTES.HOME)) from ErrorBoundary, so for non-chunk errors the fallback button now only calls resetErrorBoundary() and re-renders the same children in place. But ErrorFallback.tsx still labels that button t('label.home'). The label is now misleading, and for a deterministic render error the retry immediately re-throws — leaving the user stuck on the broken page with no working escape from the offered button (previously it took them home). Consider relabeling the button to label.retry/label.try-again, or having ErrorFallback navigate home itself when the error is not a chunk-load error.

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 0 resolved / 1 findings

Refactors redirect cookie handling to prevent users from being navigated away mid-session due to stale path storage. Consider restoring home navigation behavior for the ErrorBoundary 'Home' button.

💡 Quality: ErrorFallback "Home" button no longer navigates home

📄 openmetadata-ui/src/main/resources/ui/src/components/common/ErrorBoundary/ErrorBoundary.tsx:23-37

The diff removes onReset (which called navigate(ROUTES.HOME)) from ErrorBoundary, so for non-chunk errors the fallback button now only calls resetErrorBoundary() and re-renders the same children in place. But ErrorFallback.tsx still labels that button t('label.home'). The label is now misleading, and for a deterministic render error the retry immediately re-throws — leaving the user stuck on the broken page with no working escape from the offered button (previously it took them home). Consider relabeling the button to label.retry/label.try-again, or having ErrorFallback navigate home itself when the error is not a chunk-load error.

🤖 Prompt for agents
Code Review: Refactors redirect cookie handling to prevent users from being navigated away mid-session due to stale path storage. Consider restoring home navigation behavior for the ErrorBoundary 'Home' button.

1. 💡 Quality: ErrorFallback "Home" button no longer navigates home
   Files: openmetadata-ui/src/main/resources/ui/src/components/common/ErrorBoundary/ErrorBoundary.tsx:23-37

   The diff removes `onReset` (which called `navigate(ROUTES.HOME)`) from `ErrorBoundary`, so for non-chunk errors the fallback button now only calls `resetErrorBoundary()` and re-renders the same children in place. But `ErrorFallback.tsx` still labels that button `t('label.home')`. The label is now misleading, and for a deterministic render error the retry immediately re-throws — leaving the user stuck on the broken page with no working escape from the offered button (previously it took them home). Consider relabeling the button to `label.retry`/`label.try-again`, or having `ErrorFallback` navigate home itself when the error is not a chunk-load error.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ UI Checkstyle passed — lint findings in changed files

🔍 ESLint findings in this PR's files — 0 error(s), 34 warning(s)

Errors block the build. Warnings do not yet — they are rules whose backlog is still
being worked down, listed so this PR does not add to it. See docs/ui-code-quality-gate.md.

0 error(s), 34 warning(s) across 6 changed file(s).

Count Rule
11 i18next/no-literal-string
11 react-hooks/exhaustive-deps
3 sonarjs/no-duplicate-string
3 sonarjs/no-nested-functions
2 openmetadata-imports/no-internal-barrel-imports
1 openmetadata-imports/no-api-calls-in-iteration
1 sonarjs/cyclomatic-complexity
1 jsx-a11y/no-autofocus
1 sonarjs/no-duplicated-branches
All findings
Location Rule Message
🟡 src/components/Auth/AuthProviders/AuthProvider.test.tsx:18:1 openmetadata-imports/no-internal-barrel-imports Import the internal module directly instead of its index barrel so unrelated siblings do not enter the bundle graph.
🟡 src/components/Auth/AuthProviders/AuthProvider.test.tsx:144:71 i18next/no-literal-string disallow literal string: Logout
🟡 src/components/Auth/AuthProviders/AuthProvider.test.tsx:164:75 i18next/no-literal-string disallow literal string: Logout
🟡 src/components/Auth/AuthProviders/AuthProvider.test.tsx:196:14 i18next/no-literal-string disallow literal string: <button data-testid="login-button" onClick={() => { expect(typeof onLoginHandler).toBe('function'); onLoginHandler(); }}> Login </butto
🟡 src/components/Auth/AuthProviders/AuthProvider.test.tsx:216:17 i18next/no-literal-string disallow literal string:
ConsumerComponent
🟡 src/components/Auth/AuthProviders/AuthProvider.test.tsx:263:26 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 10 times.
🟡 src/components/Auth/AuthProviders/AuthProvider.test.tsx:531:14 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 3 times.
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:80:1 openmetadata-imports/no-internal-barrel-imports Import the internal module directly instead of its index barrel so unrelated siblings do not enter the bundle graph.
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:280:9 react-hooks/exhaustive-deps The 'onLoginHandler' function makes the dependencies of useMemo Hook (at line 919) change on every render. Move it inside the useMemo callback. Alternatively, w
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:357:6 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'navigate', 'setApplicationLoading', 'setCurrentUser', and 'setIsAuthenticated'. Either include them or remove
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:428:9 react-hooks/exhaustive-deps The 'resetUserDetails' function makes the dependencies of useMemo Hook (at line 919) change on every render. To fix this, wrap the definition of 'resetUserDetai
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:516:6 react-hooks/exhaustive-deps React Hook useEffect has a missing dependency: 'startTokenExpiryTimer'. Either include it or remove the dependency array.
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:547:6 react-hooks/exhaustive-deps React Hook useEffect has a missing dependency: 'startTokenExpiryTimer'. Either include it or remove the dependency array.
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:557:9 react-hooks/exhaustive-deps The 'handleFailedLogin' function makes the dependencies of useMemo Hook (at line 919) change on every render. Move it inside the useMemo callback. Alternatively
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:615:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'authConfig?.provider', 'handledVerifiedUser', 'navigate', 'resetUserDetails', and 'startTokenExpiryTimer'. Eit
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:651:9 react-hooks/exhaustive-deps The 'initializeAxiosInterceptors' function makes the dependencies of useMemo Hook (at line 919) change on every render. To fix this, wrap the definition of 'ini
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:722:67 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:729:23 openmetadata-imports/no-api-calls-in-iteration Avoid issuing one API request per item. Fetch at the data owner, use a bulk endpoint, or use useQueries with an intentional concurrency policy.
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:741:37 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:750:27 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:819:30 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 17 which is greater than 10 authorized.","cost":7,"secondaryLocations":[{"line":819,"column":29,"endLine":819,"endColum
🟡 src/components/Auth/AuthProviders/AuthProvider.tsx:908:6 react-hooks/exhaustive-deps React Hook useEffect has missing dependencies: 'cleanup', 'fetchAuthConfig', 'initializeAxiosInterceptors', and 'startTokenExpiryTimer'. Either include them or
🟡 src/context/PermissionProvider/PermissionProvider.test.tsx:71:48 i18next/no-literal-string disallow literal string:

Loader

🟡 src/context/PermissionProvider/PermissionProvider.test.tsx:84:37 i18next/no-literal-string disallow literal string:
Children
🟡 src/context/PermissionProvider/PermissionProvider.test.tsx:97:37 i18next/no-literal-string disallow literal string:
Children
🟡 src/context/PermissionProvider/PermissionProvider.test.tsx:114:37 i18next/no-literal-string disallow literal string:
Children
🟡 src/context/PermissionProvider/PermissionProvider.test.tsx:133:37 i18next/no-literal-string disallow literal string:
Children
🟡 src/context/PermissionProvider/PermissionProvider.test.tsx:151:37 i18next/no-literal-string disallow literal string:
Children
🟡 src/context/PermissionProvider/PermissionProvider.test.tsx:168:37 i18next/no-literal-string disallow literal string:
Children
🟡 src/context/PermissionProvider/PermissionProvider.tsx:129:6 react-hooks/exhaustive-deps React Hook useCallback has a missing dependency: 'cookieStorage'. Either include it or remove the dependency array.
🟡 src/context/PermissionProvider/PermissionProvider.tsx:273:6 react-hooks/exhaustive-deps React Hook useEffect has missing dependencies: 'currentUser', 'fetchLoggedInUserPermissions', and 'resetPermissions'. Either include them or remove the dependen
🟡 src/pages/SignUp/SignUpPage.tsx:151:15 jsx-a11y/no-autofocus The autoFocus prop should not be used, as it can reduce usability and accessibility for users.
🟡 src/utils/AuthProvider.util.ts:115:21 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 4 times.
🟡 src/utils/AuthProvider.util.ts:193:5 sonarjs/no-duplicated-branches This case's code block is the same as the block for the case on line 169.

Fix locally (fast - only checks files changed in this branch):

make ui-checkstyle-changed

@github-actions

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 66%
66.83% (79807/119401) 51.2% (48648/95014) 52.21% (14579/27921)

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit df7a6a928084c46c40fd68b2c728bc658b5cadb7 in Playwright run 31872791197, attempt 1.

✅ 796 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 51m 51s

⏱️ Max setup 3m 2s · max shard execution 19m 58s · max shard-job elapsed before upload 23m 10s · reporting 4s

🌐 223.00 requests/attempt · 2.67 app boots/UI scenario · 23.62% common-shard skew

Optimization targets still in progress:

  • Common shard skew was 23.62% (convergence target: at most 15%).
  • Browser traffic was 223 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.67 per UI scenario (2496 boots / 936 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 138 0 0 0 0 0
✅ Shard chromium-02 167 0 0 0 0 0
✅ Shard chromium-03 148 0 0 0 0 0
✅ Shard chromium-04 175 0 0 0 0 0
✅ Shard data-asset-rules-01 61 0 0 0 0 0
✅ Shard domain-isolation-01 14 0 0 0 0 0
✅ Shard global-state-01 34 0 0 0 0 0
✅ Shard import-export-01 17 0 0 0 0 0
✅ Shard ingestion-01 1 0 0 0 0 0
✅ Shard reindex-01 2 0 0 0 0 0
✅ Shard search-01 10 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Users are randomly navigated away from the page they are on (stale redirect cookie replayed)

1 participant