Repository navigation
JCU: opt-in loopback endpoint listing the SSR cache contents - #1657
Merged
Merged
Conversation
added 4 commits
October 24, 2026 11:47
…invalid port never stops the UI Review findings: non-loopback callers got Express's plain 404 instead of the catch-all route, so the feature was detectable; an invalid stats port threw synchronously before the UI started; 'enabled' could serialize as 0 or be dropped. Test now covers the query string, trailing slash and HEAD.
Refuse every X-Forwarded-* header, not just three of them; narrow the caught value (useUnknownInCatchVariables on DSpace 9 branches); never bind the stats listener to the UI port; log an invalid stats port such as a YAML string instead of ignoring it; let loopback HEAD fall through like other clients; answer 500 instead of crashing if the snapshot throws.
…he guards Review nits: exact-path check on the Express route (non-strict routing matched /app/cache-stats/ and case variants), warn when the stats are on the UI port while ui.useProxies is on, distinguish a bind failure from a later server error in the log, drop a stale peek() mention.
vidiecan
force-pushed
the
jcu/fe-ssr-cache-stats
branch
from
October 24, 2026 09:47
9522338 to
2a2e640
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Two unresolved moderate issues affect cache snapshot correctness and the documented handling of direct loopback HEAD requests.
No open issues 🎉
What changed in this PR
Adds an opt-in, loopback-restricted endpoint for inspecting per-process SSR cache contents and memory usage. The feature is disabled by default and supports UI-port or worker-specific dedicated-port operation.
Changes:
- Adds cache snapshot generation, stale-entry reporting, TTLs, HTML sizes, and process memory statistics.
- Adds configuration defaults, documentation, loopback access controls, and dedicated-server bind handling.
- Adds focused Node tests and integrates them into npm and CI workflows.
| File | Summary |
|---|---|
src/environments/environment.test.ts |
Adds test-environment cache statistics configuration. |
src/config/default-app-config.ts |
Defines disabled-by-default cache statistics settings. |
src/config/cache-config.interface.ts |
Adds the typed cache statistics configuration contract. |
src/backend/ssr-cache-stats.ts |
Implements cache snapshots, request validation, JSON handling, and the dedicated server. Moderate issue (1 vote): LRUCache.has() can remove expired entries during stale detection, mutating the cache and producing inaccurate reporting. |
server.ts |
Integrates the endpoint, cache and process metadata, worker-specific ports, and startup handling. Moderate issue (1 vote): direct loopback HEAD requests on the UI-port route fall through to the Angular fallback instead of returning the documented 404. |
scripts/test-ssr-cache-stats.js |
Adds coverage for snapshots, routing, loopback restrictions, failures, and port conflicts. |
package.json |
Adds the SSR cache statistics test command. |
config/config.example.yml |
Documents endpoint configuration and access behavior. |
.github/workflows/build.yml |
Runs the new tests in CI after linting. |
Review effort: Lite
milanmajchrak
approved these changes
Oct 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backport
Backported from: #1653 (commit 77756f2, not merged yet; this is the first customer target)
Compatibility: compatible
Manual test scenario: N/A — operator-only endpoint, off by default; the evidence below is the scenario.
Translated, not cherry-picked (DSpace 9.3 / Angular 20 / npm / lru-cache 11):
LRUCachenamed export instead ofLRUdefault,node:httpfor the branch'simport/enforce-node-protocol-usagerule,npm runin the CI step, andTS_NODE_COMPILER_OPTIONSin the test because this branch'stsconfig.jsonusesmoduleResolution: bundler, which ts-node cannot combine with its commonjs output.LRUCache.dump()on 11.x still iterates withallowStale: true(checked innode_modules/lru-cache/dist/commonjs/index.js), so stale entries are still counted.Change set
Opt-in endpoint that lists what each Node process holds in its SSR page caches (bot + anonymous): entry count vs limit, stale count, HTML bytes per entry, remaining TTL, process RSS/heap.
GET /app/cache-stats→ JSON.Config
cache.serverSide.stats(envDSPACE_CACHE_SERVERSIDE_STATS_ENABLED/_PORT), defaultenabled: false:port: 0→ served by the Express app on the UI port next to/app/health, only to requests made directly on the host: loopback socket address (req.socket.remoteAddress, notreq.ip, becausetrust proxyhonours a spoofable X-Forwarded-For) and none ofForwarded,Via,X-Real-IPor anyX-Forwarded-*, so traffic relayed by a same-host reverse proxy is refused too. Everyone else falls through to the catch-all route vianext(), so the response is byte-identical to an unknown path. Residual risk: a same-host proxy that adds none of those headers; the config comment says to use a port there. Under pm2 cluster mode (docker/dspace-ui.json:instances: max) an arbitrary worker answers.port: N→ a separatehttpserver bound to127.0.0.1:N + NODE_APP_INSTANCE, so each pm2 worker is addressable. An invalid port (incl. a YAML string), a clash with the UI port or a failed bind (EADDRINUSE) is logged and does not stop the UI.src/backend/ssr-cache-stats.ts: snapshot viaLRUCache.dump()(includes expired entries that still occupy memory whenallowStale: false;forEachwould skip them),has()marks stale; no recency/TTL side effects. Non-GET → 404.botCache/anonymousCachetypedLRUCache<string, SsrCacheEntry>instead ofany.Test:
scripts/test-ssr-cache-stats.js(node --test), wired asnpm run test:ssr-cache-statsand a CI step after lint.Out of scope: aggregating across workers, auth.
Test evidence
node dist/server/main(NODE_ENV=production, no REST backend running), three runs:Local Node 24; CI matrix (20/22) covers
node --test+ globalfetch.Notes / assumptions
docker exec <ui> wget -qO- http://127.0.0.1:<port>/app/cache-stats.npm run check-circ-depsfails on Windows before reaching madge ('bundle' is not recognized, shell quoting in the script); madge was run with the same arguments directly. Pre-existing, not touched here.