@openmrs/esm-api 10.0.1-pre.5630 → 10.0.1-pre.5642

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 14 files with swc (109.47ms)
1
+ [0] Successfully compiled: 14 files with swc (93.02ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
@@ -9,6 +9,12 @@ export type UnloadedSessionStore = {
9
9
  session: null;
10
10
  /** Set when fetching the session failed before any session had loaded. */
11
11
  error?: Error;
12
+ /**
13
+ * Set alongside `error`. `true` when the backend appears to be still starting up rather than broken:
14
+ * it redirected the session request to its initial setup page, or its gateway answered with a 502
15
+ * shortly after the page loaded.
16
+ */
17
+ initializing?: boolean;
12
18
  };
13
19
  /** @internal */
14
20
  export declare const sessionStore: import("zustand").StoreApi<SessionStore>;
@@ -1 +1 @@
1
- {"version":3,"file":"current-user.d.ts","sourceRoot":"","sources":["../src/current-user.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAiB,MAAM,SAAS,CAAC;AAEtG,MAAM,MAAM,YAAY,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;AAErE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,IAAI,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,KAAK,CAAC;IACd,OAAO,EAAE,IAAI,CAAC;IACd,0EAA0E;IAC1E,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,CAAC;AAEF,gBAAgB;AAChB,eAAO,MAAM,YAAY,0CAGvB,CAAC;AAoCH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,iBAAS,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;AAC5C;;;;;GAKG;AACH,iBAAS,cAAc,CAAC,IAAI,EAAE;IAAE,iBAAiB,EAAE,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAC7E;;;;;GAKG;AACH,iBAAS,cAAc,CAAC,IAAI,EAAE;IAAE,iBAAiB,EAAE,KAAK,CAAA;CAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;AAyBnF,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,6CAG9B;AAiBD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,QAc5C;AAwBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,yBAyBtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,SAQ/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAC3B,iBAAiB,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,EACzC,IAAI,EAAE;IAAE,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;CAAE,WAa3D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,0BAmB9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAG/E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,CAY9G;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAW/G;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE;IACd,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACrB,EACD,eAAe,CAAC,EAAE,eAAe,GAChC,OAAO,CAAC,YAAY,CAAC,CAcvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,UAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAiB3F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAY5C"}
1
+ {"version":3,"file":"current-user.d.ts","sourceRoot":"","sources":["../src/current-user.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAiB,MAAM,SAAS,CAAC;AAEtG,MAAM,MAAM,YAAY,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;AAErE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,IAAI,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,KAAK,CAAC;IACd,OAAO,EAAE,IAAI,CAAC;IACd,0EAA0E;IAC1E,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,gBAAgB;AAChB,eAAO,MAAM,YAAY,0CAGvB,CAAC;AA4CH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,iBAAS,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;AAC5C;;;;;GAKG;AACH,iBAAS,cAAc,CAAC,IAAI,EAAE;IAAE,iBAAiB,EAAE,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAC7E;;;;;GAKG;AACH,iBAAS,cAAc,CAAC,IAAI,EAAE;IAAE,iBAAiB,EAAE,KAAK,CAAA;CAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;AAyBnF,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,6CAG9B;AAiBD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,QAc5C;AAwBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,yBA0BtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,SAQ/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAC3B,iBAAiB,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,EACzC,IAAI,EAAE;IAAE,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;CAAE,WAa3D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,0BAmB9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAG/E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,CAY9G;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAW/G;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE;IACd,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACrB,EACD,eAAe,CAAC,EAAE,eAAe,GAChC,OAAO,CAAC,YAAY,CAAC,CAcvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,UAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAiB3F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAY5C"}
@@ -12,6 +12,11 @@ import { OpenmrsFetchError, openmrsFetch, restBaseUrl, sessionEndpoint } from ".
12
12
  * strictly newer than this.
13
13
  */ const sessionMaxAgeMillis = 60 * 1000;
14
14
  let lastFetchTimeMillis = 0;
15
+ /**
16
+ * How long after the first session fetch a 502 is taken to mean that the backend is still starting up. A
17
+ * gateway answers with a 502 until the backend has deployed, which can take a few minutes after a restart.
18
+ */ const startupGracePeriodMillis = 3 * 60 * 1000;
19
+ let firstFetchTimeMillis;
15
20
  let inFlightRefresh = null;
16
21
  /**
17
22
  * Fetches the session if the store is unloaded or its session is older than `sessionMaxAgeMillis`.
@@ -148,6 +153,7 @@ function isSuperUser(user) {
148
153
  * ```
149
154
  */ export function refetchCurrentUser(username, password) {
150
155
  lastFetchTimeMillis = Date.now();
156
+ firstFetchTimeMillis ?? (firstFetchTimeMillis = lastFetchTimeMillis);
151
157
  let headers = {};
152
158
  if (username && password) {
153
159
  headers['Authorization'] = `Basic ${window.btoa(`${username}:${password}`)}`;
@@ -434,6 +440,8 @@ function handleSessionResponse(result) {
434
440
  };
435
441
  sessionStore.setState(nextState, true);
436
442
  resolve(nextState);
443
+ } else if (isInitialSetupRedirect(res)) {
444
+ reject(recordSessionFailure(Error('The server is still starting up'), true));
437
445
  } else {
438
446
  reject(recordSessionFailure(Error('The session endpoint did not respond with a session')));
439
447
  }
@@ -451,6 +459,10 @@ function handleSessionResponse(result) {
451
459
  resolve(sessionStore.getState());
452
460
  return;
453
461
  }
462
+ if (isGatewayErrorDuringStartup(err)) {
463
+ reject(recordSessionFailure(err, true));
464
+ return;
465
+ }
454
466
  reportError(`Failed to fetch new session information: ${err}`);
455
467
  reject(recordSessionFailure(err));
456
468
  });
@@ -459,18 +471,35 @@ function handleSessionResponse(result) {
459
471
  function isAuthFailure(err) {
460
472
  return err instanceof OpenmrsFetchError && (err.response.status === 401 || err.response.status === 403);
461
473
  }
474
+ /**
475
+ * Until the backend has finished its initial setup, it answers every request with a redirect to its
476
+ * setup page, which `fetch()` follows, so the session request resolves with that page's HTML.
477
+ */ function isInitialSetupRedirect(res) {
478
+ if (!res?.redirected || !res.url) {
479
+ return false;
480
+ }
481
+ try {
482
+ return new URL(res.url, window.location.href).pathname.replace(/\/+$/, '').endsWith('/initialsetup');
483
+ } catch {
484
+ return false;
485
+ }
486
+ }
487
+ function isGatewayErrorDuringStartup(err) {
488
+ return err instanceof OpenmrsFetchError && err.response.status === 502 && !sessionStore.getState().loaded && firstFetchTimeMillis !== undefined && Date.now() - firstFetchTimeMillis < startupGracePeriodMillis;
489
+ }
462
490
  function isUnauthenticatedSession(body) {
463
491
  return typeof body === 'object' && body !== null && body.authenticated === false;
464
492
  }
465
493
  /**
466
494
  * A session that has already loaded is kept when a later fetch fails, since it is still the best answer
467
495
  * available. With nothing to fall back on, the error is recorded so the app shell can show an error page.
468
- */ function recordSessionFailure(err) {
496
+ */ function recordSessionFailure(err, initializing = false) {
469
497
  if (!sessionStore.getState().loaded) {
470
498
  sessionStore.setState({
471
499
  loaded: false,
472
500
  session: null,
473
- error: err instanceof Error ? err : Error(String(err))
501
+ error: err instanceof Error ? err : Error(String(err)),
502
+ initializing
474
503
  }, true);
475
504
  }
476
505
  return sessionStore.getState();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-api",
3
- "version": "10.0.1-pre.5630",
3
+ "version": "10.0.1-pre.5642",
4
4
  "license": "MPL-2.0",
5
5
  "description": "The javascript module for interacting with the OpenMRS API",
6
6
  "type": "module",
@@ -57,16 +57,16 @@
57
57
  "lodash-es": "^4.17.21"
58
58
  },
59
59
  "peerDependencies": {
60
- "@openmrs/esm-config": "^10.0.1-pre.5630",
61
- "@openmrs/esm-error-handling": "^10.0.1-pre.5630",
62
- "@openmrs/esm-globals": "^10.0.1-pre.5630",
63
- "@openmrs/esm-navigation": "^10.0.1-pre.5630"
60
+ "@openmrs/esm-config": "^10.0.1-pre.5642",
61
+ "@openmrs/esm-error-handling": "^10.0.1-pre.5642",
62
+ "@openmrs/esm-globals": "^10.0.1-pre.5642",
63
+ "@openmrs/esm-navigation": "^10.0.1-pre.5642"
64
64
  },
65
65
  "devDependencies": {
66
- "@openmrs/esm-config": "10.0.1-pre.5630",
67
- "@openmrs/esm-error-handling": "10.0.1-pre.5630",
68
- "@openmrs/esm-globals": "10.0.1-pre.5630",
69
- "@openmrs/esm-navigation": "10.0.1-pre.5630",
66
+ "@openmrs/esm-config": "10.0.1-pre.5642",
67
+ "@openmrs/esm-error-handling": "10.0.1-pre.5642",
68
+ "@openmrs/esm-globals": "10.0.1-pre.5642",
69
+ "@openmrs/esm-navigation": "10.0.1-pre.5642",
70
70
  "@swc/cli": "0.8.1",
71
71
  "@swc/core": "1.15.21",
72
72
  "@vitest/coverage-v8": "^4.1.2",
@@ -35,7 +35,7 @@ vi.mock('@openmrs/esm-error-handling', () => ({
35
35
  const mockOpenmrsFetch = vi.mocked(openmrsFetch);
36
36
  const mockReportError = vi.mocked(reportError);
37
37
 
38
- function createAuthFailure(status: number, responseBody: unknown = null) {
38
+ function createFetchError(status: number, responseBody: unknown = null) {
39
39
  return new OpenmrsFetchError('/openmrs/ws/rest/v1/session', { status } as Response, responseBody as any, Error());
40
40
  }
41
41
 
@@ -592,8 +592,8 @@ describe('refetchCurrentUser', () => {
592
592
  const error = new Error('Bad gateway');
593
593
  mockOpenmrsFetch.mockRejectedValue(error);
594
594
 
595
- await expect(refetchCurrentUser()).rejects.toEqual({ loaded: false, session: null, error });
596
- expect(sessionStore.getState()).toEqual({ loaded: false, session: null, error });
595
+ await expect(refetchCurrentUser()).rejects.toEqual({ loaded: false, session: null, error, initializing: false });
596
+ expect(sessionStore.getState()).toEqual({ loaded: false, session: null, error, initializing: false });
597
597
  });
598
598
 
599
599
  it('should keep an already-loaded session when a later fetch fails', async () => {
@@ -615,11 +615,62 @@ describe('refetchCurrentUser', () => {
615
615
  loaded: false,
616
616
  error: new Error('The session endpoint did not respond with a session'),
617
617
  });
618
+ expect(sessionStore.getState()).toMatchObject({ initializing: false });
619
+ });
620
+
621
+ it('should record that the server is starting up when the session request is redirected to initial setup', async () => {
622
+ mockOpenmrsFetch.mockResolvedValue({
623
+ ...createMockFetchResponse(undefined),
624
+ redirected: true,
625
+ url: 'http://localhost/openmrs/initialsetup',
626
+ });
627
+
628
+ await expect(refetchCurrentUser()).rejects.toMatchObject({ loaded: false, session: null, initializing: true });
629
+ expect(sessionStore.getState()).toMatchObject({
630
+ loaded: false,
631
+ initializing: true,
632
+ error: new Error('The server is still starting up'),
633
+ });
634
+ });
635
+
636
+ it('should record that the server is starting up when the gateway responds with a 502 soon after the first fetch', async () => {
637
+ mockReportError.mockClear();
638
+ mockOpenmrsFetch.mockRejectedValue(createFetchError(502));
639
+
640
+ await expect(refetchCurrentUser()).rejects.toMatchObject({ loaded: false, initializing: true });
641
+ expect(mockReportError).not.toHaveBeenCalled();
642
+ });
643
+
644
+ it('should record an ordinary error when the gateway still responds with a 502 minutes after the first fetch', async () => {
645
+ vi.useFakeTimers();
646
+
647
+ try {
648
+ mockOpenmrsFetch.mockRejectedValue(createFetchError(502));
649
+ await refetchCurrentUser().catch(() => {});
650
+
651
+ vi.setSystemTime(Date.now() + 3 * 60 * 1000);
652
+ mockReportError.mockClear();
653
+
654
+ await expect(refetchCurrentUser()).rejects.toMatchObject({ loaded: false, initializing: false });
655
+ expect(mockReportError).toHaveBeenCalled();
656
+ } finally {
657
+ vi.useRealTimers();
658
+ }
659
+ });
660
+
661
+ it('should keep an already-loaded session and report the error when the gateway responds with a 502', async () => {
662
+ const mockSession: Session = { authenticated: true, sessionId: 'test-session', user: {} as LoggedInUser };
663
+ sessionStore.setState({ loaded: true, session: mockSession }, true);
664
+ mockReportError.mockClear();
665
+ mockOpenmrsFetch.mockRejectedValue(createFetchError(502));
666
+
667
+ await expect(refetchCurrentUser()).rejects.toEqual({ loaded: true, session: mockSession });
668
+ expect(mockReportError).toHaveBeenCalled();
618
669
  });
619
670
 
620
671
  it('should use the unauthenticated session in the body of a 401 response', async () => {
621
672
  const unauthenticated = { authenticated: false, sessionId: 'abc', allowedLocales: ['en', 'fr'] };
622
- mockOpenmrsFetch.mockRejectedValue(createAuthFailure(401, unauthenticated));
673
+ mockOpenmrsFetch.mockRejectedValue(createFetchError(401, unauthenticated));
623
674
 
624
675
  await expect(refetchCurrentUser()).resolves.toEqual({ loaded: true, session: unauthenticated });
625
676
  });
@@ -628,7 +679,7 @@ describe('refetchCurrentUser', () => {
628
679
  'should record a logged-out session when the session endpoint responds with %i',
629
680
  async (status) => {
630
681
  mockReportError.mockClear();
631
- mockOpenmrsFetch.mockRejectedValue(createAuthFailure(status));
682
+ mockOpenmrsFetch.mockRejectedValue(createFetchError(status));
632
683
 
633
684
  await expect(refetchCurrentUser()).resolves.toEqual({
634
685
  loaded: true,
@@ -644,7 +695,7 @@ describe('refetchCurrentUser', () => {
644
695
  );
645
696
 
646
697
  it('should not leave readers waiting on the failed request after an auth failure', async () => {
647
- mockOpenmrsFetch.mockRejectedValueOnce(createAuthFailure(401));
698
+ mockOpenmrsFetch.mockRejectedValueOnce(createFetchError(401));
648
699
  await refetchCurrentUser();
649
700
 
650
701
  await expect(getCurrentUser()).resolves.toEqual({ authenticated: false, sessionId: '' });
@@ -901,7 +952,7 @@ describe('logout', () => {
901
952
  });
902
953
 
903
954
  it('should treat a session the server has already ended as logged out', async () => {
904
- mockOpenmrsFetch.mockRejectedValue(createAuthFailure(401));
955
+ mockOpenmrsFetch.mockRejectedValue(createFetchError(401));
905
956
 
906
957
  await expect(logout()).resolves.toBeUndefined();
907
958
  expect(sessionStore.getState()).toEqual(loggedOut);
@@ -17,6 +17,12 @@ export type UnloadedSessionStore = {
17
17
  session: null;
18
18
  /** Set when fetching the session failed before any session had loaded. */
19
19
  error?: Error;
20
+ /**
21
+ * Set alongside `error`. `true` when the backend appears to be still starting up rather than broken:
22
+ * it redirected the session request to its initial setup page, or its gateway answered with a 502
23
+ * shortly after the page loaded.
24
+ */
25
+ initializing?: boolean;
20
26
  };
21
27
 
22
28
  /** @internal */
@@ -33,6 +39,14 @@ export const sessionStore = createGlobalStore<SessionStore>('session', {
33
39
  const sessionMaxAgeMillis = 60 * 1000;
34
40
 
35
41
  let lastFetchTimeMillis = 0;
42
+
43
+ /**
44
+ * How long after the first session fetch a 502 is taken to mean that the backend is still starting up. A
45
+ * gateway answers with a 502 until the backend has deployed, which can take a few minutes after a restart.
46
+ */
47
+ const startupGracePeriodMillis = 3 * 60 * 1000;
48
+
49
+ let firstFetchTimeMillis: number | undefined;
36
50
  let inFlightRefresh: Promise<SessionStore> | null = null;
37
51
 
38
52
  /**
@@ -236,6 +250,7 @@ function isSuperUser(user: { roles: Array<Role> }) {
236
250
  */
237
251
  export function refetchCurrentUser(username?: string, password?: string) {
238
252
  lastFetchTimeMillis = Date.now();
253
+ firstFetchTimeMillis ??= lastFetchTimeMillis;
239
254
  let headers = {};
240
255
  if (username && password) {
241
256
  headers['Authorization'] = `Basic ${window.btoa(`${username}:${password}`)}`;
@@ -557,6 +572,8 @@ function handleSessionResponse(result: Promise<FetchResponse<Session>>) {
557
572
  const nextState: SessionStore = { loaded: true, session: res.data };
558
573
  sessionStore.setState(nextState, true);
559
574
  resolve(nextState);
575
+ } else if (isInitialSetupRedirect(res)) {
576
+ reject(recordSessionFailure(Error('The server is still starting up'), true));
560
577
  } else {
561
578
  reject(recordSessionFailure(Error('The session endpoint did not respond with a session')));
562
579
  }
@@ -573,6 +590,11 @@ function handleSessionResponse(result: Promise<FetchResponse<Session>>) {
573
590
  return;
574
591
  }
575
592
 
593
+ if (isGatewayErrorDuringStartup(err)) {
594
+ reject(recordSessionFailure(err, true));
595
+ return;
596
+ }
597
+
576
598
  reportError(`Failed to fetch new session information: ${err}`);
577
599
  reject(recordSessionFailure(err));
578
600
  });
@@ -583,6 +605,32 @@ function isAuthFailure(err: unknown): err is OpenmrsFetchError {
583
605
  return err instanceof OpenmrsFetchError && (err.response.status === 401 || err.response.status === 403);
584
606
  }
585
607
 
608
+ /**
609
+ * Until the backend has finished its initial setup, it answers every request with a redirect to its
610
+ * setup page, which `fetch()` follows, so the session request resolves with that page's HTML.
611
+ */
612
+ function isInitialSetupRedirect(res: FetchResponse | undefined) {
613
+ if (!res?.redirected || !res.url) {
614
+ return false;
615
+ }
616
+
617
+ try {
618
+ return new URL(res.url, window.location.href).pathname.replace(/\/+$/, '').endsWith('/initialsetup');
619
+ } catch {
620
+ return false;
621
+ }
622
+ }
623
+
624
+ function isGatewayErrorDuringStartup(err: unknown) {
625
+ return (
626
+ err instanceof OpenmrsFetchError &&
627
+ err.response.status === 502 &&
628
+ !sessionStore.getState().loaded &&
629
+ firstFetchTimeMillis !== undefined &&
630
+ Date.now() - firstFetchTimeMillis < startupGracePeriodMillis
631
+ );
632
+ }
633
+
586
634
  function isUnauthenticatedSession(body: unknown): body is Session {
587
635
  return typeof body === 'object' && body !== null && (body as Session).authenticated === false;
588
636
  }
@@ -591,13 +639,14 @@ function isUnauthenticatedSession(body: unknown): body is Session {
591
639
  * A session that has already loaded is kept when a later fetch fails, since it is still the best answer
592
640
  * available. With nothing to fall back on, the error is recorded so the app shell can show an error page.
593
641
  */
594
- function recordSessionFailure(err: unknown): SessionStore {
642
+ function recordSessionFailure(err: unknown, initializing = false): SessionStore {
595
643
  if (!sessionStore.getState().loaded) {
596
644
  sessionStore.setState(
597
645
  {
598
646
  loaded: false,
599
647
  session: null,
600
648
  error: err instanceof Error ? err : Error(String(err)),
649
+ initializing,
601
650
  },
602
651
  true,
603
652
  );