From d4f0e9c59fe875185a2ffc3b9faf7b57ae571a92 Mon Sep 17 00:00:00 2001 From: 47th <161213233+flcrom@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:04:12 +0530 Subject: [PATCH 1/2] fix: prefer live environment before offline fallback --- sdk/index.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index e262278..80853a6 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -566,7 +566,7 @@ export class Flagsmith { * Calling this method concurrently while the environment is being fetched will not cause additional requests. */ async getEnvironment(): Promise { - if (this.offlineHandler) { + if (this.offlineMode && this.offlineHandler) { return this.offlineHandler.getEnvironment(); } if (this.environment) { @@ -575,7 +575,14 @@ export class Flagsmith { if (!this.environmentPromise) { this.environmentPromise = this.fetchEnvironment(); } - return this.environmentPromise; + try { + return await this.environmentPromise; + } catch (error) { + if (this.offlineHandler) { + return this.offlineHandler.getEnvironment(); + } + throw error; + } } private async getEnvironmentFromApi() { From b0ff97e6f51be8f2c3abfb2353be7b0cb63e11f9 Mon Sep 17 00:00:00 2001 From: 47th <161213233+flcrom@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:05:13 +0530 Subject: [PATCH 2/2] test: cover offline fallback and recovery --- tests/sdk/flagsmith.test.ts | 76 +++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/sdk/flagsmith.test.ts b/tests/sdk/flagsmith.test.ts index acaf939..c4ccd94 100644 --- a/tests/sdk/flagsmith.test.ts +++ b/tests/sdk/flagsmith.test.ts @@ -538,3 +538,79 @@ test('Flags.fromEvaluationResult throws error when metadata.id is missing', () = 'This indicates a bug in the SDK, please report it.' ); }); + +function offlineFallbackClient() { + const offline = environmentModel(JSON.parse(offlineEnvironmentJSON)); + const remote = JSON.parse(offlineEnvironmentJSON); + remote.feature_states[0].feature_state_value = 'remote-value'; + const handler = new BaseOfflineHandler(); + vi.spyOn(handler, 'getEnvironment').mockReturnValue(offline); + const request = vi.fn(async () => new Response(JSON.stringify(remote), { status: 200 })); + const client = flagsmith({ + environmentKey: 'ser.key', + enableLocalEvaluation: true, + offlineHandler: handler, + retries: 0, + fetch: request + }); + return { client, handler, request }; +} + +test('local evaluation prefers the live environment over the offline handler', async () => { + const { client, handler, request } = offlineFallbackClient(); + try { + const flags = await client.getEnvironmentFlags(); + expect(flags.getFeatureValue('some_feature')).toBe('remote-value'); + const identityFlags = await client.getIdentityFlags('identity'); + expect(identityFlags.getFeatureValue('some_feature')).toBe('remote-value'); + expect(request).toHaveBeenCalledTimes(1); + expect(handler.getEnvironment).not.toHaveBeenCalled(); + } finally { + client.close(); + } +}); + +test('local evaluation recovers after using the offline handler on fetch failure', async () => { + const { client, handler, request } = offlineFallbackClient(); + request.mockRejectedValueOnce(new Error('unavailable')); + try { + const fallback = await client.getEnvironmentFlags(); + expect(fallback.getFeatureValue('some_feature')).toBe('offline-value'); + expect(handler.getEnvironment).toHaveBeenCalledTimes(1); + const recovered = await client.getIdentityFlags('identity'); + expect(recovered.getFeatureValue('some_feature')).toBe('remote-value'); + expect(request).toHaveBeenCalledTimes(2); + expect(handler.getEnvironment).toHaveBeenCalledTimes(1); + } finally { + client.close(); + } +}); + +test('local evaluation keeps the live environment after a failed refresh', async () => { + const { client, handler, request } = offlineFallbackClient(); + try { + await client.updateEnvironment(); + request.mockRejectedValueOnce(new Error('unavailable')); + await client.updateEnvironment(); + const flags = await client.getEnvironmentFlags(); + expect(flags.getFeatureValue('some_feature')).toBe('remote-value'); + expect(handler.getEnvironment).not.toHaveBeenCalled(); + } finally { + client.close(); + } +}); + +test('local evaluation without an offline handler still surfaces fetch failure', async () => { + const { client, request } = offlineFallbackClient(); + client.offlineHandler = undefined; + const failure = new Error('unavailable'); + request.mockRejectedValue(failure); + try { + await expect(client.getEnvironment()).rejects.toBe(failure); + await expect(client.getEnvironmentFlags()).rejects.toThrow( + 'getEnvironmentFlags failed and no default flag handler was provided' + ); + } finally { + client.close(); + } +});