API Configuration, better support via runtime env vars#3380
Conversation
OpenAPI ChangesNo changes detected Unexpected changes? Ensure your branch is up-to-date with |
39317cf to
bffc27f
Compare
There was a problem hiding this comment.
Pull request overview
This PR moves Learn and MITx Online API clients toward runtime configuration so frontend/API requests can use environment values available after deployment rather than values captured during build.
Changes:
- Adds configurable axios singletons and runtime API client setup for Learn and MITx Online.
- Updates Next instrumentation and Jest setup to configure API clients before use.
- Moves basket replacement redirect behavior into
mainand normalizes paginatednextURLs to use configured axiosbaseURL.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
frontends/main/src/bootstrap/api.ts |
Adds runtime API client bootstrap logic. |
frontends/main/src/bootstrap/api.test.ts |
Tests bootstrap URL/credential configuration behavior. |
frontends/main/src/instrumentation.ts |
Configures server-side API clients during instrumentation registration. |
frontends/main/src/instrumentation-client.ts |
Configures browser API clients before hydration. |
frontends/main/src/test-utils/setupJest.tsx |
Configures API clients for frontend Jest tests. |
frontends/main/src/common/mitxonline/index.ts |
Improves missing legacy MITx URL invariant message. |
frontends/main/src/common/mitxonline/useReplaceBasketItem.ts |
Adds app-level basket replacement hook with cart redirect. |
frontends/main/src/common/mitxonline/useReplaceBasketItem.test.tsx |
Tests basket replacement and redirect behavior. |
frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx |
Uses the new app-level basket replacement hook. |
frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx |
Uses the new app-level basket replacement hook. |
frontends/main/src/page-components/EnrollmentDialogs/CourseEnrollmentDialog.tsx |
Uses the new app-level basket replacement hook. |
frontends/main/src/page-components/EnrollmentDialogs/ProgramEnrollmentDialog.tsx |
Uses the new app-level basket replacement hook. |
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx |
Uses the new app-level basket replacement hook. |
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ModuleCard.tsx |
Uses the new app-level basket replacement hook. |
frontends/api/package.json |
Exports the new runtime configuration module. |
frontends/api/src/configurableAxios.ts |
Adds globally shared configurable axios singleton helper. |
frontends/api/src/axios.ts |
Converts Learn axios to configurable runtime client. |
frontends/api/src/mitxonline/axios.ts |
Adds configurable MITx Online axios client. |
frontends/api/src/runtime/index.ts |
Adds API client configuration/reset APIs. |
frontends/api/src/runtime/index.test.ts |
Tests runtime client configuration behavior. |
frontends/api/src/runtime/urls.ts |
Adds helper to strip origins from paginated API URLs. |
frontends/api/src/runtime/urls.test.ts |
Tests URL origin stripping. |
frontends/api/src/clients.ts |
Uses axios baseURL instead of generated client base path. |
frontends/api/src/mitxonline/clients.ts |
Uses configurable MITx axios and empty generated base path. |
frontends/api/src/mitxonline/hooks/baskets/index.ts |
Removes redirecting basket replacement from API hook layer. |
frontends/api/src/hooks/learningResources/queries.ts |
Normalizes infinite-query next URLs. |
frontends/api/src/hooks/learningResources/index.test.ts |
Tests normalized next-page requests. |
frontends/api/src/hooks/learningPaths/queries.ts |
Normalizes infinite-query next URLs. |
frontends/api/src/hooks/learningPaths/index.test.ts |
Updates next-page normalization test coverage. |
frontends/api/src/hooks/userLists/queries.ts |
Normalizes infinite-query next URLs. |
frontends/api/src/hooks/userLists/index.test.ts |
Tests normalized next-page requests. |
frontends/api/src/test-utils/setupJest.ts |
Configures API clients for API workspace tests. |
frontends/api/src/test-utils/urls.ts |
Builds Learn test URLs from configured axios base URL. |
frontends/api/src/test-utils/urls.test.ts |
Tests Learn test URL runtime configuration. |
frontends/api/src/mitxonline/test-utils/urls.ts |
Builds MITx test URLs from configured axios base URL. |
frontends/api/src/mitxonline/test-utils/urls.test.ts |
Tests MITx test URL runtime configuration. |
frontends/api/src/test-utils/mock-requests-example.test.ts |
Adds coverage for resolving relative axios URLs against baseURL. |
| const NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL = | ||
| process.env.NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL | ||
| invariant(NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL) | ||
| invariant( | ||
| NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL, | ||
| "NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL must be set", |
There was a problem hiding this comment.
Env vars are still read at build time. This just moves them out of the api workspace, so they can be easily changed to runtime in #3364
Your suggestion is completely unrelated to your comment, but is worthwhile.
gumaerc
left a comment
There was a problem hiding this comment.
This all looks good and is working for me, just one very minor question:
| instance.defaults.withCredentials = config.withCredentials | ||
| } | ||
|
|
||
| const isConfigured = (): boolean => Boolean(instance.defaults.baseURL) |
There was a problem hiding this comment.
The xsrfCookieName and withCredentials props are also set when running applyConfig. Do we want to ensure those are also set to for this to be true, or is it more about the bare minimum needed for Axios to technically function?
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace non-null assertions in bootstrapApiClients with a requireEnv helper backed by tiny-invariant. Errors now name the missing env var instead of the downstream config field. configureApiClients keeps its strict signature so callers retain accurate type hints and compile-time checks. Also document why bootstrapApiClients() must run at module scope in providers.tsx — child components fire React Query during render, before any effect would run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
configureApiClients now throws on any second call, eliminating the hand-maintained (or isEqual-based) deep equality check and its maintenance footgun. First-wins idempotency moves to a single explicit gate in bootstrapApiClients(), using a new isApiClientsConfigured() predicate. resetApiClientsForTests remains the test escape hatch. This separates concerns: configureApiClients writes config once, isApiClientsConfigured answers "have we?", and bootstrapApiClients owns the "exactly once across triggers" policy in one obvious place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
configureApiClients's signature already guarantees ApiClientsConfig. The isRecord/typeof guards re-checked what the type promises and the tests that exercised them had to cast through 'as unknown as' to even reach the runtime branch — defending JS-level inputs that the typed surface won't produce. What stays: normalizeBaseUrl still throws when the post-normalize value is empty (the slash-only case), which is a real invariant (an empty baseURL would silently break the test mock's URL matching). Also: useReplaceBasketItem.test.tsx now restores process.env in beforeEach so the 'throws at module load' test is robust to reordering instead of relying on Jest source order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filename suggested coverage of queries.ts but the file only re-tested toRelativeApiUrl, which runtime/urls.test.ts already covers. The jest.resetModules + dynamic import + process.env mutation scaffolding was solving a problem that doesn't exist: toRelativeApiUrl is a pure function and never reads env. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
runtime/index.test.ts: drop Object.isFrozen and Reflect.set return-value assertions from the frozen-config test. They were verifying JS engine semantics around Object.freeze. The post-mutation read-back is the behavioral guard that actually catches freezeConfig regressions: if freeze were removed, the Reflect.set calls would succeed and the read-back would observe the mutated values. bootstrap/api.test.ts: drop the mitxonline propagation block from the server-URL test. The browser-URL test already asserts the full config shape; the server test now narrows to the Learn server-fallback URL, which is the only thing that varies between the two cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Delete frontends/api/src/clients.test.ts — tautological test that
asserted a hardcoded constant; flagged by jest/no-export rule.
- Drop the orphaned BASE_PATH export from clients.ts; it was widened
only to feed the deleted test.
- Convert stray require("../runtime") in api setupJest.ts to a static
import; flagged by @typescript-eslint/no-require-imports.
- Add an eslint-disable for the legitimate require() inside
jest.isolateModules in useReplaceBasketItem.test.tsx — the callback
must be synchronous, so require is the only way to load the module.
The remaining warning in useContractDashboardData.ts is pre-existing
and not in scope for this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cc/mock-axios-adapter mock changed makeRequest from positional
(method, url, body) to a single object arg ({ method, url, body }).
Three new tests added on this branch (the pagination tests for
learningResources and userLists, and the resolved-URL example in
mock-requests-example) still used the positional form; migrate them
to expect.objectContaining({ method, url }) to match the adapter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- normalizeBaseUrl: replace the trailing-slash regex /\/+$/ with a linear index scan. CodeQL flagged the regex as polynomial ReDoS — it backtracks O(n^2) on inputs with many trailing slashes, and baseUrl is env-derived (uncontrolled per static analysis). Behavior is unchanged: strip trailing slashes, reject an empty result. - Prettier: wrap two long template-literal arrows in the test URL builders (the getApiBaseUrl() prefix pushed them past print width). - Add an eslint-disable for the jest.isolateModules require() in instrumentation-client.test.ts (sync callback; require is required). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- runtime config: assert configureApiClients propagates xsrfCookieName and withCredentials (not just baseURL) to both axios singletons — the CSRF/credentials wiring this branch establishes was previously unverified. - runtime config: drop the freeze test's axios-default assertions; they could not fail for the freeze invariant (axios holds an independent copy) and the propagation is now covered above. - useReplaceBasketItem: add failure-path tests proving no add + no redirect when the sync clear never succeeds or the async clear rejects. The sync case also pins the clear->add->redirect nesting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- instrumentation-client: run bootstrapApiClients() after Sentry.init so a missing-env failure during bootstrap is reported to Sentry instead of crashing before error monitoring is armed. Still runs at module load, before hydration, so React Query hooks remain safe on first paint. - bootstrap/api.ts: correct the first-wins comment — providers.tsx no longer calls bootstrap, so the server configures once at startup (instrumentation.ts) and the browser once (instrumentation-client.ts, re-entrant under Fast Refresh). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the near-identical Learn and MITx axios scaffolds into a single createConfigurableAxios(registryKey) factory: axios.create + the fail-loud 'not configured' request interceptor + apply/reset helpers. Park the instance on globalThis under Symbol.for(registryKey) so it is a true per-process singleton. Next.js evaluates the instrumentation runtime and the server render runtime as separate module graphs; without sharing, configureApiClients() from instrumentation would mutate only instrumentation's copy and leave the render graph unconfigured. The global symbol registry gives every module graph the same slot. The configured-state guard now derives from instance.defaults.baseURL presence rather than a separate boolean, removing one of the duplicated sources of truth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make configurableAxios the sole owner of the globalThis singleton mechanism and have the runtime config derive all state through it, so the configured instances and the "configured?" signal can no longer disagree across Next.js's separate module graphs. - Extract the globalThis/Symbol.for helper into configurableAxios and drop the standalone globalSingleton module; runtime no longer keeps its own module-local config state (which was reset per graph / per Fast Refresh while the instances persisted). - Derive isApiClientsConfigured() from the shared instances; configureApiClients returns void. - Remove getApiClientsConfig / client getConfig: their only consumers were the url test-utils, which only need the base URL. They now read it straight off the configured axios instance (the single source of truth). - Fix bootstrap/api.ts requireEnv: read each NEXT_PUBLIC_* var as a static literal so Next inlines it client-side. Dynamic process.env[name] lookups yield undefined in the browser, throwing even when the var is set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- bootstrap: coalesce empty NEXT_SERVER_MITOL_API_BASE_URL with `||` so a blank value (env/codespaces.env) falls back to the public URL instead of throwing during normalization; add regression test - runtime: collapse identical LearnApiConfig/MitxOnlineApiConfig into the canonical ConfigurableAxiosConfig - runtime/urls: restore hq#10999 rationale as a docstring on toRelativeApiUrl - remove tautological instrumentation-client.test.ts - move lodash back to devDependencies (test-utils only) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The three infinite-hook tests (learningPaths, learningResources, userLists) were near-identical copy-paste asserting the same toRelativeApiUrl wiring. Extract the body into assertNormalizesPaginationNext in hooks/test-utils, so each hook keeps a thin co-located test (drift protection) without the boilerplate. Also allow devDependencies in **/test-utils.tsx (the glob only listed the .ts variant). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
isConfigured previously inferred "configured" from defaults.baseURL, which conflates "configure ran" with "a default happens to be set". Track it explicitly with a Symbol.for-keyed flag on the (already-singleton) instance — cross-graph visible without a separate globalThis entry — and give createConfigurableAxios an explicit ConfigurableAxiosClient return type so the internal marker doesn't leak. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4b8c8df to
ef025a1
Compare
Following the rebase onto main (#3380 API runtime config), finish moving every NEXT_PUBLIC_* read off build-time inlining and onto the runtime env() helper, and align test setup with the runtime model. Runtime-env correctness: - bootstrap/api.ts: read NEXT_PUBLIC_* via env()/requiredEnv() instead of build-inlined process.env. Without this the standalone image crash-loops at boot (client + server) because configureApiClients relied on values that are empty in an env-less build. - OnboardingPage, YouTubeIframePlayer, urls.ts: migrate the remaining reads to env()/requiredEnv() at call sites (no module-scope invariants). - env.ts: guard the client process.env fallback (typeof process check) so an absent optional NEXT_PUBLIC_* var can't throw ReferenceError in the browser. - backgroundImages.ts: drop the redundant NEXT_PUBLIC_OPTIMIZE_IMAGES check; getImageProps already respects images.unoptimized. Test & validation hygiene: - Move NEXT_PUBLIC_* test values out of the shared jest setup into main's setup (only main app code reads them); api configures clients with hardcoded test URLs; leaf packages need none. - validateEnv: require NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL at startup (relocates #3380's module-load invariant) and drop the now-obsolete "throws at module load" test. - Remove dead NEXT_PUBLIC_OPTIMIZE_IMAGES from env/frontend.env. No build-inlined NEXT_PUBLIC_* reads remain in app code. Verified: typecheck, eslint, and the full frontend test suites pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Following the rebase onto main (#3380 API runtime config), finish moving every NEXT_PUBLIC_* read off build-time inlining and onto the runtime env() helper, and align test setup with the runtime model. Runtime-env correctness: - bootstrap/api.ts: read NEXT_PUBLIC_* via env()/requiredEnv() instead of build-inlined process.env. Without this the standalone image crash-loops at boot (client + server) because configureApiClients relied on values that are empty in an env-less build. - OnboardingPage, YouTubeIframePlayer, urls.ts: migrate the remaining reads to env()/requiredEnv() at call sites (no module-scope invariants). - env.ts: guard the client process.env fallback (typeof process check) so an absent optional NEXT_PUBLIC_* var can't throw ReferenceError in the browser. - backgroundImages.ts: drop the redundant NEXT_PUBLIC_OPTIMIZE_IMAGES check; getImageProps already respects images.unoptimized. Test & validation hygiene: - Move NEXT_PUBLIC_* test values out of the shared jest setup into main's setup (only main app code reads them); api configures clients with hardcoded test URLs; leaf packages need none. - validateEnv: require NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL at startup (relocates #3380's module-load invariant) and drop the now-obsolete "throws at module load" test. - Remove dead NEXT_PUBLIC_OPTIMIZE_IMAGES from env/frontend.env. No build-inlined NEXT_PUBLIC_* reads remain in app code. Verified: typecheck, eslint, and the full frontend test suites pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What are the relevant tickets?
Description (What does it do?)
This PR changes how we initialize API clients. Previously they were initialized within the
apiworkspace. Now they are initialized inmain. More importantly, this sets up the mechanism for us to switch to runtime initialization in #3364Previously, API clients were initialized in the
apiworkspace reading environment variables directly at build time. After #3364, all environment variables must be read at runtime, not buildtime.Now clients are configureed:
instrumentation.ts, for server-side rendering and server routes (like the sitemap)instrumentation-client.tsfor client-side hydration (the process by which React attaches itself to pre-rendered HTML).How can this be tested?
Notes
AI Added a lot of new tests, which seem fine if not overly likely to break. The production code diff is about
+300 / -100.