Infinite route-loader spinner: per-navigation invalidateRootCache() deadlocks getEndpointMapAt when the root endpoint goes stale

Open
#5,855 6 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@naomigassler is already working on this.

Since Jul 9, 2026.

  • #5936 by @naomigassler — closed without merging

Assessment

This issue has not been assessed yet.

Description

affects: 10.x affects: 8.x affects: 9.x bug performance / caching

Describe the bug

After the UI has been open a while, navigating to / revisiting a page can leave the app stuck on the route-loader spinner (<ds-loading> in ds-base-root, driven by AppComponent.isRouteLoading$) forever. The shell stays responsive (navbar, sidebar, overlays work); a full reload or clicking any other in-app link recovers it, but the routed content never renders. While stuck: nothing is pending on the network, the NgRx store is frozen, and CPU/memory are idle — a logical deadlock, not a loop or leak. It is much harder to reproduce with DevTools open and far worse in production than on a local dev server (cf. #3584, #3697).

To Reproduce

Authenticated, ideally behind a reverse proxy / with non-trivial REST latency:

  1. Load a content page (e.g. a Community/Collection).
  2. Navigate around / wait a bit.
  3. Navigate back (or keep navigating). The spinner appears and never resolves; reload or any other link recovers it. Higher REST latency → more reliable (essentially every revisit through a proxy).

Expected behavior

Navigation completes and the page renders; a transiently-stale root endpoint must not wedge navigation indefinitely.

Root cause

  1. BrowserInitService.listenForRouteChanges() calls rootDataService.invalidateRootCache() on every NavigationStart → RequestService.setStaleByHref('/server/api') marks the root request stale (regardless of its TTL).
  2. HALEndpointService.getEndpointMapAt() — used by getEndpoint(), which every data request needs — does filter(rd => !rd.isStale), discarding the stale root; its tap(... getEndpointMapAt()) re-request does not reliably produce a fresh one, so it never emits.
  3. So getEndpoint() never resolves → the route resolver never completes → the router never emits NavigationEnd → isRouteLoading$ (cleared only on NavigationEnd/Cancel) stays true → the route loader spins forever, store frozen.

Evidence (frozen store at the hang): root /server/api is SuccessStale with TTL not expired (explicitly invalidated, not aged out); 0 requests pending (nothing re-fetching); the only RequestStaleAction caller is setStaleByHref ← invalidateRootCache ← the NavigationStart subscription. Consistent with #2669 (invalidateRootCache ↔ HALEndpointService stale interaction) after #2510 (7.6.1) made HAL endpoint requests stale-able.

Proposed fix

The root endpoint map is static between navigations, so the per-NavigationStart invalidation is unnecessary. Removing it eliminates the deadlock (backend availability is still established at app init and surfaces through normal request failures):

 import {
-  NavigationStart,
   Router,
 } from '@angular/router';
@@ listenForRouteChanges()
   protected listenForRouteChanges(): void {
-    // we'll always be too late for the first NavigationStart event with the router subscribe below,
-    // so this statement is for the very first route operation.
+    // Invalidate the root endpoint cache once, for the very first route operation.
     this.rootDataService.invalidateRootCache();
-
-    this.router.events.pipe(
-      filter(event => event instanceof NavigationStart),
-    ).subscribe(() => {
-      this.rootDataService.invalidateRootCache();
-    });
   }

(Alternative, if the per-navigation backend check is to be kept: make getEndpointMapAt tolerate a stale-but-completed root, e.g. filter(rd => !rd.isStale || rd.hasCompleted), so a stale endpoint map is used while the tap() refreshes it — fixing the deadlock without changing the invalidation cadence.)

Verified: the change removes the hang and the E2E suite is unaffected.

Environment

  • dspace-angular 9.3 (Angular 20.3); listenForRouteChanges()/getEndpointMapAt() are unchanged on main as of this report.
  • DSpace 9.3 REST backend behind an nginx reverse proxy; CSR (SSR disabled); reproduced in Safari and Firefox.

Related

#2669, #2510, #3584, #3697, #3888

Dominant language
TypeScript
Stars
180
Forks
577
Avg merge
2d 1h
Merged PRs (30d)
100

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from DSpace/dspace-angular

All issues in DSpace/dspace-angular

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.