Skip to content

Commit 5cd31ca

Browse files
authored
Merge pull request #256 from frankieyan/fix/handle-uncaught-fetch-exception
fix: Handle uncaught fetch exception
2 parents 4f4372a + 4713411 commit 5cd31ca

File tree

2 files changed

+183
-81
lines changed

2 files changed

+183
-81
lines changed

flagsmith-core.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,10 @@ const Flagsmith = class {
479479
}
480480
if (shouldFetchFlags) {
481481
// We want to resolve init since we have cached flags
482-
this.getFlags();
482+
483+
this.getFlags().catch((error) => {
484+
this.onError?.(error)
485+
})
483486
}
484487
} else {
485488
if (!preventFetch) {

test/init.test.ts

Lines changed: 179 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
// Sample test
2-
import { defaultState, environmentID, getFlagsmith, getStateToCheck, identityState } from './test-constants';
3-
import { promises as fs } from 'fs'
2+
import { waitFor } from '@testing-library/react';
3+
import { defaultState, getFlagsmith, getStateToCheck, identityState } from './test-constants';
4+
import { promises as fs } from 'fs';
45

56
describe('Flagsmith.init', () => {
6-
77
beforeEach(() => {
88
// Avoid mocks, but if you need to add them here
99
});
1010
test('should initialize with expected values', async () => {
11-
const onChange = jest.fn()
12-
const {flagsmith,initConfig, AsyncStorage,mockFetch} = getFlagsmith({onChange})
11+
const onChange = jest.fn();
12+
const { flagsmith, initConfig, AsyncStorage, mockFetch } = getFlagsmith({ onChange });
1313
await flagsmith.init(initConfig);
1414

1515
expect(flagsmith.environmentID).toBe(initConfig.environmentID);
@@ -19,14 +19,17 @@ describe('Flagsmith.init', () => {
1919
expect(onChange).toHaveBeenCalledTimes(1);
2020
expect(onChange).toHaveBeenCalledWith(
2121
{},
22-
{"flagsChanged": Object.keys(defaultState.flags), "isFromServer": true, "traitsChanged": null},
23-
{"error": null, "isFetching": false, "isLoading": false, "source": "SERVER"}
22+
{ flagsChanged: Object.keys(defaultState.flags), isFromServer: true, traitsChanged: null },
23+
{ error: null, isFetching: false, isLoading: false, source: 'SERVER' },
2424
);
25-
expect(getStateToCheck(flagsmith.getState())).toEqual(defaultState)
25+
expect(getStateToCheck(flagsmith.getState())).toEqual(defaultState);
2626
});
2727
test('should initialize with identity', async () => {
28-
const onChange = jest.fn()
29-
const {flagsmith,initConfig, AsyncStorage,mockFetch} = getFlagsmith({onChange, identity:"test_identity"})
28+
const onChange = jest.fn();
29+
const { flagsmith, initConfig, AsyncStorage, mockFetch } = getFlagsmith({
30+
onChange,
31+
identity: 'test_identity',
32+
});
3033
await flagsmith.init(initConfig);
3134

3235
expect(flagsmith.environmentID).toBe(initConfig.environmentID);
@@ -36,16 +39,27 @@ describe('Flagsmith.init', () => {
3639
expect(onChange).toHaveBeenCalledTimes(1);
3740
expect(onChange).toHaveBeenCalledWith(
3841
{},
39-
{"flagsChanged": Object.keys(defaultState.flags), "isFromServer": true, "traitsChanged": expect.arrayContaining(Object.keys(identityState.evaluationContext.identity.traits))},
40-
{"error": null, "isFetching": false, "isLoading": false, "source": "SERVER"}
42+
{
43+
flagsChanged: Object.keys(defaultState.flags),
44+
isFromServer: true,
45+
traitsChanged: expect.arrayContaining(Object.keys(identityState.evaluationContext.identity.traits)),
46+
},
47+
{ error: null, isFetching: false, isLoading: false, source: 'SERVER' },
4148
);
42-
expect(getStateToCheck(flagsmith.getState())).toEqual(identityState)
49+
expect(getStateToCheck(flagsmith.getState())).toEqual(identityState);
4350
});
4451
test('should initialize with identity and traits', async () => {
45-
const onChange = jest.fn()
46-
const testIdentityWithTraits = `test_identity_with_traits`
47-
const {flagsmith,initConfig, AsyncStorage,mockFetch} = getFlagsmith({onChange, identity:testIdentityWithTraits, traits:{number_trait:1, string_trait:"Example"}})
48-
mockFetch.mockResolvedValueOnce({status: 200, text: () => fs.readFile(`./test/data/identities_${testIdentityWithTraits}.json`, 'utf8')})
52+
const onChange = jest.fn();
53+
const testIdentityWithTraits = `test_identity_with_traits`;
54+
const { flagsmith, initConfig, AsyncStorage, mockFetch } = getFlagsmith({
55+
onChange,
56+
identity: testIdentityWithTraits,
57+
traits: { number_trait: 1, string_trait: 'Example' },
58+
});
59+
mockFetch.mockResolvedValueOnce({
60+
status: 200,
61+
text: () => fs.readFile(`./test/data/identities_${testIdentityWithTraits}.json`, 'utf8'),
62+
});
4963

5064
await flagsmith.init(initConfig);
5165

@@ -56,97 +70,182 @@ describe('Flagsmith.init', () => {
5670
expect(onChange).toHaveBeenCalledTimes(1);
5771
expect(onChange).toHaveBeenCalledWith(
5872
{},
59-
{"flagsChanged": Object.keys(defaultState.flags), "isFromServer": true, "traitsChanged": ["number_trait","string_trait"]},
60-
{"error": null, "isFetching": false, "isLoading": false, "source": "SERVER"}
73+
{
74+
flagsChanged: Object.keys(defaultState.flags),
75+
isFromServer: true,
76+
traitsChanged: ['number_trait', 'string_trait'],
77+
},
78+
{ error: null, isFetching: false, isLoading: false, source: 'SERVER' },
6179
);
6280
expect(getStateToCheck(flagsmith.getState())).toEqual({
6381
...identityState,
6482
evaluationContext: {
6583
...identityState.evaluationContext,
6684
identity: {
6785
...identityState.evaluationContext.identity,
68-
identifier: testIdentityWithTraits
86+
identifier: testIdentityWithTraits,
6987
},
7088
},
71-
})
89+
});
7290
});
7391
test('should reject initialize with identity no key', async () => {
74-
const onChange = jest.fn()
75-
const {flagsmith,initConfig} = getFlagsmith({onChange, evaluationContext:{environment:{apiKey: ""}}})
92+
const onChange = jest.fn();
93+
const { flagsmith, initConfig } = getFlagsmith({
94+
onChange,
95+
evaluationContext: { environment: { apiKey: '' } },
96+
});
7697
await expect(flagsmith.init(initConfig)).rejects.toThrow(Error);
7798
});
7899
test('should reject initialize with identity bad key', async () => {
79-
const onChange = jest.fn()
80-
const {flagsmith,initConfig,mockFetch} = getFlagsmith({onChange, environmentID:"bad"})
81-
mockFetch.mockResolvedValueOnce({status: 404, text: async () => ''})
100+
const onChange = jest.fn();
101+
const { flagsmith, initConfig, mockFetch } = getFlagsmith({ onChange, environmentID: 'bad' });
102+
mockFetch.mockResolvedValueOnce({ status: 404, text: async () => '' });
82103
await expect(flagsmith.init(initConfig)).rejects.toThrow(Error);
83104
});
84105
test('identifying with new identity should not carry over previous traits for different identity', async () => {
85-
const onChange = jest.fn()
86-
const identityA = `test_identity_a`
87-
const identityB = `test_identity_b`
88-
const {flagsmith,initConfig,mockFetch} = getFlagsmith({onChange, identity:identityA, traits: {a:`example`}})
89-
mockFetch.mockResolvedValueOnce({status: 200, text: () => fs.readFile(`./test/data/identities_${identityA}.json`, 'utf8')})
106+
const onChange = jest.fn();
107+
const identityA = `test_identity_a`;
108+
const identityB = `test_identity_b`;
109+
const { flagsmith, initConfig, mockFetch } = getFlagsmith({
110+
onChange,
111+
identity: identityA,
112+
traits: { a: `example` },
113+
});
114+
mockFetch.mockResolvedValueOnce({
115+
status: 200,
116+
text: () => fs.readFile(`./test/data/identities_${identityA}.json`, 'utf8'),
117+
});
90118
await flagsmith.init(initConfig);
91-
expect(flagsmith.getTrait("a")).toEqual(`example`)
92-
mockFetch.mockResolvedValueOnce({status: 200, text: () => fs.readFile(`./test/data/identities_${identityB}.json`, 'utf8')})
93-
await flagsmith.identify(identityB)
94-
expect(flagsmith.getTrait("a")).toEqual(undefined)
95-
mockFetch.mockResolvedValueOnce({status: 200, text: () => fs.readFile(`./test/data/identities_${identityA}.json`, 'utf8')})
96-
await flagsmith.identify(identityA)
97-
expect(flagsmith.getTrait("a")).toEqual(`example`)
98-
mockFetch.mockResolvedValueOnce({status: 200, text: () => fs.readFile(`./test/data/identities_${identityB}.json`, 'utf8')})
99-
await flagsmith.identify(identityB)
100-
expect(flagsmith.getTrait("a")).toEqual(undefined)
119+
expect(flagsmith.getTrait('a')).toEqual(`example`);
120+
mockFetch.mockResolvedValueOnce({
121+
status: 200,
122+
text: () => fs.readFile(`./test/data/identities_${identityB}.json`, 'utf8'),
123+
});
124+
await flagsmith.identify(identityB);
125+
expect(flagsmith.getTrait('a')).toEqual(undefined);
126+
mockFetch.mockResolvedValueOnce({
127+
status: 200,
128+
text: () => fs.readFile(`./test/data/identities_${identityA}.json`, 'utf8'),
129+
});
130+
await flagsmith.identify(identityA);
131+
expect(flagsmith.getTrait('a')).toEqual(`example`);
132+
mockFetch.mockResolvedValueOnce({
133+
status: 200,
134+
text: () => fs.readFile(`./test/data/identities_${identityB}.json`, 'utf8'),
135+
});
136+
await flagsmith.identify(identityB);
137+
expect(flagsmith.getTrait('a')).toEqual(undefined);
101138
});
102139
test('identifying with transient identity should request the API correctly', async () => {
103-
const onChange = jest.fn()
104-
const testTransientIdentity = `test_transient_identity`
140+
const onChange = jest.fn();
141+
const testTransientIdentity = `test_transient_identity`;
105142
const evaluationContext = {
106-
identity: {identifier: testTransientIdentity, transient: true}
107-
}
108-
const {flagsmith,initConfig,mockFetch} = getFlagsmith({onChange, evaluationContext})
109-
mockFetch.mockResolvedValueOnce({status: 200, text: () => fs.readFile(`./test/data/identities_${testTransientIdentity}.json`, 'utf8')})
143+
identity: { identifier: testTransientIdentity, transient: true },
144+
};
145+
const { flagsmith, initConfig, mockFetch } = getFlagsmith({ onChange, evaluationContext });
146+
mockFetch.mockResolvedValueOnce({
147+
status: 200,
148+
text: () => fs.readFile(`./test/data/identities_${testTransientIdentity}.json`, 'utf8'),
149+
});
110150
await flagsmith.init(initConfig);
111-
expect(mockFetch).toHaveBeenCalledWith(`https://edge.api.flagsmith.com/api/v1/identities/?identifier=${testTransientIdentity}&transient=true`,
112-
expect.objectContaining({method: 'GET'}),
113-
)
151+
expect(mockFetch).toHaveBeenCalledWith(
152+
`https://edge.api.flagsmith.com/api/v1/identities/?identifier=${testTransientIdentity}&transient=true`,
153+
expect.objectContaining({ method: 'GET' }),
154+
);
114155
});
115156
test('identifying with transient traits should request the API correctly', async () => {
116-
const onChange = jest.fn()
117-
const testIdentityWithTransientTraits = `test_identity_with_transient_traits`
157+
const onChange = jest.fn();
158+
const testIdentityWithTransientTraits = `test_identity_with_transient_traits`;
118159
const evaluationContext = {
119160
identity: {
120161
identifier: testIdentityWithTransientTraits,
121162
traits: {
122-
number_trait: {value: 1},
123-
string_trait: {value: 'Example'},
124-
transient_trait: {value: 'Example', transient: true},
125-
}
126-
}
127-
}
128-
const {flagsmith,initConfig,mockFetch} = getFlagsmith({onChange, evaluationContext})
129-
mockFetch.mockResolvedValueOnce({status: 200, text: () => fs.readFile(`./test/data/identities_${testIdentityWithTransientTraits}.json`, 'utf8')})
163+
number_trait: { value: 1 },
164+
string_trait: { value: 'Example' },
165+
transient_trait: { value: 'Example', transient: true },
166+
},
167+
},
168+
};
169+
const { flagsmith, initConfig, mockFetch } = getFlagsmith({ onChange, evaluationContext });
170+
mockFetch.mockResolvedValueOnce({
171+
status: 200,
172+
text: () => fs.readFile(`./test/data/identities_${testIdentityWithTransientTraits}.json`, 'utf8'),
173+
});
130174
await flagsmith.init(initConfig);
131-
expect(mockFetch).toHaveBeenCalledWith('https://edge.api.flagsmith.com/api/v1/identities/',
132-
expect.objectContaining({method: 'POST', body: JSON.stringify({
133-
"identifier": testIdentityWithTransientTraits,
134-
"traits": [
135-
{
136-
"trait_key": "number_trait",
137-
"trait_value": 1
138-
},
139-
{
140-
"trait_key": "string_trait",
141-
"trait_value": "Example"
142-
},
143-
{
144-
"trait_key": "transient_trait",
145-
"trait_value": "Example",
146-
"transient": true
147-
}
148-
]
149-
})}),
150-
)
175+
expect(mockFetch).toHaveBeenCalledWith(
176+
'https://edge.api.flagsmith.com/api/v1/identities/',
177+
expect.objectContaining({
178+
method: 'POST',
179+
body: JSON.stringify({
180+
identifier: testIdentityWithTransientTraits,
181+
traits: [
182+
{
183+
trait_key: 'number_trait',
184+
trait_value: 1,
185+
},
186+
{
187+
trait_key: 'string_trait',
188+
trait_value: 'Example',
189+
},
190+
{
191+
trait_key: 'transient_trait',
192+
trait_value: 'Example',
193+
transient: true,
194+
},
195+
],
196+
}),
197+
}),
198+
);
199+
});
200+
test('should not reject but call onError, when the API cannot be reached with the cache populated', async () => {
201+
const onError = jest.fn();
202+
const { flagsmith, initConfig, AsyncStorage } = getFlagsmith({
203+
cacheFlags: true,
204+
fetch: async () => {
205+
return Promise.resolve({ text: () => Promise.resolve('Mocked fetch error'), ok: false, status: 401 });
206+
},
207+
onError,
208+
});
209+
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify(defaultState));
210+
await flagsmith.init(initConfig);
211+
212+
expect(getStateToCheck(flagsmith.getState())).toEqual(defaultState);
213+
214+
await waitFor(() => {
215+
expect(onError).toHaveBeenCalledTimes(1);
216+
});
217+
expect(onError).toHaveBeenCalledWith(new Error('Mocked fetch error'));
218+
});
219+
test('should not reject when the API cannot be reached but default flags are set', async () => {
220+
const { flagsmith, initConfig } = getFlagsmith({
221+
defaultFlags: defaultState.flags,
222+
cacheFlags: true,
223+
fetch: async () => {
224+
return Promise.resolve({ text: () => Promise.resolve('Mocked fetch error'), ok: false, status: 401 });
225+
},
226+
});
227+
await flagsmith.init(initConfig);
228+
229+
expect(getStateToCheck(flagsmith.getState())).toEqual(defaultState);
230+
});
231+
test('should not reject but call onError, when the identities/ API cannot be reached with the cache populated', async () => {
232+
const onError = jest.fn();
233+
const { flagsmith, initConfig, AsyncStorage } = getFlagsmith({
234+
evaluationContext: identityState.evaluationContext,
235+
cacheFlags: true,
236+
fetch: async () => {
237+
return Promise.resolve({ text: () => Promise.resolve('Mocked fetch error'), ok: false, status: 401 });
238+
},
239+
onError,
240+
});
241+
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify(identityState));
242+
await flagsmith.init(initConfig);
243+
244+
expect(getStateToCheck(flagsmith.getState())).toEqual(identityState);
245+
246+
await waitFor(() => {
247+
expect(onError).toHaveBeenCalledTimes(1);
248+
});
249+
expect(onError).toHaveBeenCalledWith(new Error('Mocked fetch error'));
151250
});
152251
});

0 commit comments

Comments
 (0)