Skip to content

fix: prevent route crash when subscription or permissions prefetch fails#6047

Open
devin-ai-integration[bot] wants to merge 1 commit intomainfrom
devin/1776276354-fix-access-management-undefined-component
Open

fix: prevent route crash when subscription or permissions prefetch fails#6047
devin-ai-integration[bot] wants to merge 1 commit intomainfrom
devin/1776276354-fix-access-management-undefined-component

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

Context

On self-hosted instances (particularly those without a pro license), navigating to the organization or project access management pages can crash with:

TypeError: Cannot read properties of undefined (reading 'component')

The _inject-org-details route middleware runs three sequential ensureQueryData calls in its beforeLoad hook. If either the subscription or permissions prefetch throws (e.g. due to a network issue or an edge case in the license/plan endpoint), the entire beforeLoad rejects. This prevents TanStack Router from initializing the route tree, which surfaces as the cryptic 'component' TypeError in Match.js.

Before: All three prefetches were hard-awaited — any single failure crashed the route.

After: The org-details fetch remains a hard await (it's critical — without it nothing can render). The subscription and permissions fetches are wrapped in Promise.allSettled so a failure in either is non-fatal. The downstream components already use useSuspenseQuery with the same query keys, so if the prefetch cache is empty they will independently re-fetch and suspend gracefully.

Important things for reviewers to verify

  • Suspense boundary coverage: When ensureQueryData is skipped (rejected), the consuming useSuspenseQuery in useOrgPermission / useOrganization will suspend. Confirm there is an adequate <Suspense> boundary in the route tree (TanStack Router's Match component provides one when PendingComponent or wrapInSuspense is configured, and the router has defaultErrorComponent: ErrorPage).
  • Silent failures: Promise.allSettled swallows rejections without logging. Consider whether a console.warn on rejected results would aid future debugging.
  • No change to critical path: The fetchOrganizationById call is still a hard await — if that fails, the route will still error as before. Only subscription + permissions are made resilient.

Steps to verify the change

  1. Deploy to a self-hosted instance without a pro license key
  2. Log in and navigate to Organization → Access Management
  3. Confirm the page loads without the TypeError crash
  4. Confirm that users, identities, groups, and roles tabs render correctly
  5. Navigate to a project → Access Control and verify the same

Type

  • Fix
  • Feature
  • Improvement
  • Breaking
  • Docs
  • Chore

Checklist

Link to Devin session: https://app.devin.ai/sessions/32d726ea8334497a8349cdc2d126ddf2

Wrap the subscription and permissions ensureQueryData calls in
Promise.allSettled so that a failure in either prefetch does not crash
the entire route tree. The components using this data already have their
own useSuspenseQuery hooks that will re-fetch independently if the
prefetch cache is empty.

This fixes a TypeError ('Cannot read properties of undefined (reading
component)') that occurs on self-hosted instances without a pro license
when accessing the access management pages.

Co-Authored-By: jake <jake@infisical.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@maidul98
Copy link
Copy Markdown
Collaborator

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — the core fix is correct and the nits are already covered by inline comments.

Extended reasoning...

Overview\nSingle-file change to that wraps the subscription and permissions calls in instead of hard-awaiting them sequentially. The org-details fetch (critical path) remains a hard await.\n\n### Security risks\nNo security-sensitive code touched. No auth, crypto, or permissions logic changed — only the route loading behavior for non-critical prefetches.\n\n### Level of scrutiny\nLow: this is a focused, mechanical fix for a real crash on self-hosted instances without a pro license. The change is minimal and the intent is clear. is the standard idiom for fire-and-forget parallel prefetches.\n\n### Other factors\nThe two reported bugs are minor nits (missing and an inaccurate comment about vs ), both already noted by the PR author in the description itself. Neither affects correctness of the fix. The inline comments will prompt the author to address them. The overall behavior change — preventing a hard crash from a non-critical prefetch failure — is strictly an improvement.

Comment on lines +68 to +79
// Prefetch subscription and permissions data but don't block route
// loading if they fail — the components using this data have their
// own useQuery hooks that will retry independently.
await Promise.allSettled([
context.queryClient.ensureQueryData({
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId),
queryFn: () => fetchOrgSubscription(organizationId)
}),
context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserOrgPermissions({ orgId: organizationId }),
queryFn: () => fetchUserOrgPermissions({ orgId: organizationId })
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The Promise.allSettled result is never inspected, so rejections from fetchOrgSubscription or fetchUserOrgPermissions are completely silent — no logging, no alerting — despite an analogous console.warn already existing at line 58 for similar non-fatal failures. Additionally, the comment on lines 68–70 says these components use "useQuery hooks that will retry independently", but both SubscriptionContext.tsx and OrgPermissionContext.tsx actually use useSuspenseQuery, which throws to the nearest React error boundary on persistent failure rather than silently retrying; users who hit a persistent error land on ErrorPage, which has only a "Back To Home" link with no retry button.

Extended reasoning...

Issue 1: Silent rejection swallowing

The await Promise.allSettled([...]) on line 71 discards the returned PromiseSettledResult[] array entirely — the result is never bound to a variable and never inspected. This means if fetchOrgSubscription or fetchUserOrgPermissions rejects (e.g. due to a network error, a broken license endpoint on a self-hosted instance, or an auth hiccup), the failure is completely invisible: no console output, no metrics, no alerting. The PR description itself calls this out: 'Promise.allSettled swallows rejections without logging. Consider whether a console.warn on rejected results would aid future debugging.'

Why existing code does not prevent it: The same file already has an established pattern for non-fatal failures. At line 58, the token-exchange catch block emits console.warn("Failed to automatically exchange token for organization:", error). The Promise.allSettled block is directly analogous — intentionally non-fatal — yet omits any equivalent logging. The inconsistency means subscription/permission prefetch failures are invisible while token-exchange failures are surfaced.

Concrete proof of the gap: Suppose fetchOrgSubscription rejects with a 500 from the license endpoint. Promise.allSettled catches the rejection internally and stores { status: 'rejected', reason: Error(...) } in the results array. Because that array is immediately discarded, the error is gone. The route continues loading. No developer — in local dev, staging, or production — has any signal that the prefetch failed. Filtering rejected results and emitting a console.warn (matching the pattern at line 58) would surface these failures for free.

Issue 2: Inaccurate comment — useSuspenseQuery vs useQuery

The comment on lines 68–70 reads: "the components using this data have their own useQuery hooks that will retry independently." Both downstream context files use useSuspenseQuery, not useQuery. These hooks have fundamentally different failure semantics: useQuery returns { status: 'error' } and lets the component decide how to render; useSuspenseQuery throws the error to the nearest React error boundary on persistent failure. With retry: 1 configured on the QueryClient, if both the prefetch and a single component-level retry fail, useSuspenseQuery throws to the router's defaultErrorComponent: ErrorPage. The ErrorPage component only renders a "Back To Home" link — no retry button — leaving users stuck.

Why this matters beyond a nit: The phrase "retry independently" implies graceful, silent recovery. A future maintainer relying on this comment might not add proper error handling (e.g. an error boundary with a retry button at the subscription/permission level), assuming the hooks silently recover. The comment should accurately describe that useSuspenseQuery is in use and that persistent failures surface to the error boundary.

Suggested fix: (1) Capture and inspect the Promise.allSettled results, logging any rejections via console.warn consistent with line 58. (2) Update the comment to accurately state useSuspenseQuery is used and that persistent failures throw to the error boundary rather than silently retrying.

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