fix: fail the admin boot when /api/config does not answer - #921
Merged
Conversation
The fetch swallowed its failure and carried on with env.js. In prod that file is a ConfigMap holding only configEndpoint and KEYCLOAK_CLIENT_ID, so a swallowed failure builds Keycloak with no URL at all; in dev and test the image's env.js supplies the dev loginproxy, so staff would be sent to the wrong identity provider with the sysadmin check decided by dev's roles and the banner still reading "dev". init() is already awaited before Keycloak starts, so a throw means no IdP is ever contacted. A 200 is guarded too: KEYCLOAK_URL and KEYCLOAK_REALM both default to null in the model and the controller hydrates rather than leans, so a half-seeded document is a valid response whose nulls would clobber working values. eagle-public deliberately keeps its swallow — it is unauthenticated and every env.js default it falls back to is still correct. Two related fixes here because they share the file: /admin/env.js was served immutable for a year, since `location = /env.js` never matches a request arriving as /admin/env.js and the static-asset regex won instead; and the build now greps the built env.js, because sed exits 0 when it matches nothing.
danieltruong
requested review from
Ckoelewyn,
tolkamps1 and
tom0827
as code owners
August 14, 2026 17:35
This was referenced Aug 14, 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.
What
Three changes that share
ConfigService,main.tsand the Dockerfile:/api/configfetch now fails the boot instead of being swallowed./admin/env.jsstops being served with a one-year immutable cache.configEndpointrewrite did not take.Paired with bcgov/eagle-api#839, which stops a config failure being cached for a minute.
1. Hard-fail the fetch
fetchRemoteConfig()caught its own failure, logged, and carried on withenv.js.main.tsalready awaitsconfigService.init()strictly beforekeycloakService.init(), so a throw means Keycloak is never constructed and nobody is sent to an identity provider. The ordering was right; only the swallow was wrong.What the swallow costs, per environment:
env.jsis theeagle-admin-env-jsConfigMap, mountedsubPathover the image's copy, and it holds onlyconfigEndpointandKEYCLOAK_CLIENT_ID. Verified live. So there is no fallback at all: Keycloak gets constructed withurl: undefinedand the console breaks silently.env.jssupplieshttps://dev.loginproxy.gov.bc.ca/auth. Worse than nothing: staff authenticate against the dev identity provider, thesysadmincheck atkeycloak.service.ts:130-134is decided by dev's role assignments against production data, andBANNER_COLOUR/ENVIRONMENTstill read "dev" so the console looks like dev while writing to prod. Onelogger.errorin a browser console is the only trace.A failed boot is the cheaper outcome, so
main.tsgains a.catchthat logs and replacesapp-rootwith onerole="alert"sentence. Two details there:keycloak.service.ts:98callsreject()with no argument, so a loginproxy outage would otherwise printundefinedunder a message blaming config.app-rootrather than appending.index.htmlships a placeholder spinner inside it and nothing else removes it, so appending would leave the page saying "loading" and "could not start" at once.The 200 is guarded too
Transport-level handling is not enough.
KEYCLOAK_URLandKEYCLOAK_REALMboth default tonullin eagle-api's Config model, the controller hydrates rather than.lean()s, and its payload filter skipsundefinedbut notnull— so a partially seeded document is a valid 200 whose nulls merge overenv.jsand construct Keycloak withurl: null. Same failure, no error status to catch it by. Hence the explicit check before the merge.KEYCLOAK_CLIENT_IDpreservation stays. It is not redundant: the live dev, test and prod payloads all serveeagle-api-console, and that line is the only thing stopping admin authenticating as the API's client.Why admin and not eagle-public
eagle-public deliberately keeps its swallow. It is unauthenticated — no Keycloak, no
withCredentialsin the tree — and everyenv.jsdefault it falls back to is still correct (API_PATH='/api',ANALYTICS_API_URL='/analytics',SEARCH_API_PATH=''falling back to eagle-api). OnlyENVIRONMENTgoes stale, which is cosmetic. Hard-failing it would turn a survivable eagle-api blip into an outage ofprojects.eao.gov.bc.caand buy nothing.The asymmetry is the point, not an inconsistency.
2.
/admin/env.jswas cached for a yearVerified live:
curl -sI …/admin/env.js→max-age=31536000, public, immutable. The no-cache rule waslocation = /env.jsnested insidelocation / { alias /tmp/app/dist/; }, but requests arrive as/admin/env.js, so the exact match never fired and the sibling~* \.(js|css|…)$block won. Returning staff kept a year-old file — which, after this PR, would be a year-oldconfigEndpoint.Now
location ~ ^/(admin/)?env\.js$. One caveat worth knowing at review time: nginx picks the first matching regex in file order, whereas an exact match is order-independent. This block must stay declared above the static-asset regex; moving it below silently restores the one-year cache. Noted in the wiki.Tested against a real
nginx:1.27-alpineusing this Dockerfile's own config:/admin/env.jsmax-age=31536000, immutableno-cache, no-store, must-revalidate/admin/styles.css/admin/index.html,/admin/3. Build guard
sedexits 0 when it matches nothing, so renamingconfigEndpointwould ship an image running production on baked-in dev values with a green build. The build now greps the built copy, which additionally catchesenv.jsdropping out of theangular.jsonassets list — that ships noenv.jsat all, and in admin meansKEYCLOAK_ENABLEDundefined, falsy, every user bounced to/login.The
sedis also anchored on the fullwindow.__env.assignment so it and the guard reference an identical string: rename the key and both fail together.Verified by building the image (passes) and by building a throwaway copy with the key renamed (fails, with the message).
Backwards compatibility
Safe to deploy alone, ahead of the cutover, in any environment.
The new throw is dormant until cutover:
/api/configis currently answered by nginx from a file on local disk, so it effectively cannot fail while the pod is up. When it does become live, it needsKEYCLOAK_URLandKEYCLOAK_REALM— confirmed present in dev and testrproxy-config, confirmed by livecurlin prod, and emitted unconditionally byeao-nginx/helm/rproxy/templates/configmap.yaml:18-19so a values omission cannot drop them.Tests
New
src/app/services/config.service.spec.ts, 6 cases: non-2xx rejects; network error rejects; a 200 missingKEYCLOAK_URLrejects and no merge happens; a 200 withKEYCLOAK_URL: nullrejects;configEndpoint: falsedoes not fetch and resolves (this is what keeps local dev working); and a server-senteagle-api-consoledoes not overwriteeagle-admin-console.Lint clean, 49 passing.