Skip to content

Commit 2ce41da

Browse files
authored
transient error handling (404, 403) (#40)
1 parent 8680b8e commit 2ce41da

File tree

9 files changed

+112
-16
lines changed

9 files changed

+112
-16
lines changed

configcatclient/configfetcher.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ class Status(IntEnum):
3333

3434

3535
class FetchResponse(object):
36-
def __init__(self, status, entry, error=None):
36+
def __init__(self, status, entry, error=None, is_transient_error=False):
3737
self._status = status
3838
self.entry = entry
3939
self.error = error
40+
self.is_transient_error = is_transient_error
4041

4142
def is_fetched(self):
4243
"""Gets whether a new configuration value was fetched or not.
@@ -65,8 +66,8 @@ def not_modified(cls):
6566
return FetchResponse(Status.NotModified, ConfigEntry.empty)
6667

6768
@classmethod
68-
def failure(cls, error):
69-
return FetchResponse(Status.Failure, ConfigEntry.empty, error)
69+
def failure(cls, error, is_transient_error):
70+
return FetchResponse(Status.Failure, ConfigEntry.empty, error, is_transient_error)
7071

7172

7273
class ConfigFetcher(object):
@@ -169,19 +170,23 @@ def _fetch(self, etag):
169170
return FetchResponse.success(ConfigEntry(config, response_etag, get_utc_now_seconds_since_epoch()))
170171
elif response.status_code == 304:
171172
return FetchResponse.not_modified()
173+
elif response.status_code in [404, 403]:
174+
error = 'Double-check your SDK Key at https://app.configcat.com/sdkkey. ' \
175+
'Received unexpected response: %s' % str(response)
176+
self.log.error(error)
177+
return FetchResponse.failure(error, False)
172178
else:
173179
raise (requests.HTTPError(response))
174180
except HTTPError as e:
175-
error = 'Double-check your SDK Key at https://app.configcat.com/sdkkey. ' \
176-
'Received unexpected response: %s' % str(e.response)
181+
error = 'Unexpected HTTP response was received: %s' % str(e.response)
177182
self.log.error(error)
178-
return FetchResponse.failure(error)
183+
return FetchResponse.failure(error, True)
179184
except Timeout:
180185
error = 'Request timed out. Timeout values: [connect: {}s, read: {}s]'.format(
181-
self._config_fetcher.get_connect_timeout(), self._config_fetcher.get_read_timeout())
186+
self.get_connect_timeout(), self.get_read_timeout())
182187
self.log.error(error)
183-
return FetchResponse.failure(error)
188+
return FetchResponse.failure(error, True)
184189
except Exception as e:
185190
error = 'Exception occurred during fetching: ' + str(e)
186191
self.log.error(error)
187-
return FetchResponse.failure(error)
192+
return FetchResponse.failure(error, True)

configcatclient/configservice.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ def _fetch_if_older(self, time, prefer_cache=False):
132132
self._cached_entry = response.entry
133133
self._write_cache(response.entry)
134134
self._hooks.invoke_on_config_changed(response.entry.config.get(FEATURE_FLAGS))
135-
elif response.is_not_modified():
135+
elif (response.is_not_modified() or not response.is_transient_error) and \
136+
not self._cached_entry.is_empty():
136137
self._cached_entry.fetch_time = utils.get_utc_now_seconds_since_epoch()
137138
self._write_cache(self._cached_entry)
138139

configcatclient/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
CONFIGCATCLIENT_VERSION = "6.0.3"
1+
CONFIGCATCLIENT_VERSION = "7.0.0"

configcatclienttests/mocks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def __init__(self, error):
9797
self._error = error
9898

9999
def get_configuration(self, etag=''):
100-
return FetchResponse.failure(self._error)
100+
return FetchResponse.failure(self._error, True)
101101

102102

103103
class ConfigFetcherWaitMock(ConfigFetcher):

configcatclienttests/test_configcatclient.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import datetime
12
import logging
23
import unittest
34
import requests
45

56
from configcatclient import ConfigCatClientException
67
from configcatclient.configcatclient import ConfigCatClient
8+
from configcatclient.constants import VALUE, COMPARATOR, COMPARISON_ATTRIBUTE, COMPARISON_VALUE
79
from configcatclient.user import User
810
from configcatclient.configcatoptions import ConfigCatOptions
911
from configcatclient.pollingmode import PollingMode
12+
from configcatclient.utils import get_utc_now
1013
from configcatclienttests.mocks import ConfigCacheMock, TEST_OBJECT
1114

1215
# Python2/Python3 support
@@ -143,6 +146,37 @@ def details_by_key(all_details, key):
143146

144147
client.close()
145148

149+
def test_get_value_details(self):
150+
with mock.patch.object(requests, 'get') as request_get:
151+
response_mock = Mock()
152+
request_get.return_value = response_mock
153+
response_mock.json.return_value = TEST_OBJECT
154+
response_mock.status_code = 200
155+
response_mock.headers = {}
156+
157+
client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
158+
client.force_refresh()
159+
160+
user = User("[email protected]")
161+
details = client.get_value_details('testStringKey', '', user)
162+
163+
self.assertEqual('fake1', details.value)
164+
self.assertEqual('testStringKey', details.key)
165+
self.assertEqual('id1', details.variation_id)
166+
self.assertFalse(details.is_default_value)
167+
self.assertIsNone(details.error)
168+
self.assertIsNone(details.matched_evaluation_percentage_rule)
169+
self.assertEqual('fake1', details.matched_evaluation_rule[VALUE])
170+
self.assertEqual(2, details.matched_evaluation_rule[COMPARATOR])
171+
self.assertEqual('Identifier', details.matched_evaluation_rule[COMPARISON_ATTRIBUTE])
172+
self.assertEqual('@test1.com', details.matched_evaluation_rule[COMPARISON_VALUE])
173+
self.assertEqual(str(user), str(details.user))
174+
now = get_utc_now()
175+
self.assertGreaterEqual(now, details.fetch_time)
176+
self.assertLessEqual(now, details.fetch_time + + datetime.timedelta(seconds=1))
177+
178+
client.close()
179+
146180
def test_cache_key(self):
147181
client1 = ConfigCatClient.get('test1', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
148182
config_cache=ConfigCacheMock()))

configcatclienttests/test_configfetcher.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def test_http_error(self):
6868
fetcher = ConfigFetcher(sdk_key='', log=log, mode='m')
6969
fetch_response = fetcher.get_configuration()
7070
self.assertTrue(fetch_response.is_failed())
71+
self.assertTrue(fetch_response.is_transient_error)
7172
self.assertTrue(fetch_response.entry.is_empty())
7273

7374
def test_exception(self):
@@ -76,6 +77,35 @@ def test_exception(self):
7677
fetcher = ConfigFetcher(sdk_key='', log=log, mode='m')
7778
fetch_response = fetcher.get_configuration()
7879
self.assertTrue(fetch_response.is_failed())
80+
self.assertTrue(fetch_response.is_transient_error)
81+
self.assertTrue(fetch_response.entry.is_empty())
82+
83+
def test_404_failed_fetch_response(self):
84+
with mock.patch.object(requests, 'get') as request_get:
85+
response_mock = Mock()
86+
request_get.return_value = response_mock
87+
response_mock.json.return_value = {}
88+
response_mock.status_code = 404
89+
response_mock.headers = {}
90+
fetcher = ConfigFetcher(sdk_key='', log=log, mode='m')
91+
fetch_response = fetcher.get_configuration()
92+
self.assertTrue(fetch_response.is_failed())
93+
self.assertFalse(fetch_response.is_transient_error)
94+
self.assertFalse(fetch_response.is_fetched())
95+
self.assertTrue(fetch_response.entry.is_empty())
96+
97+
def test_403_failed_fetch_response(self):
98+
with mock.patch.object(requests, 'get') as request_get:
99+
response_mock = Mock()
100+
request_get.return_value = response_mock
101+
response_mock.json.return_value = {}
102+
response_mock.status_code = 403
103+
response_mock.headers = {}
104+
fetcher = ConfigFetcher(sdk_key='', log=log, mode='m')
105+
fetch_response = fetcher.get_configuration()
106+
self.assertTrue(fetch_response.is_failed())
107+
self.assertFalse(fetch_response.is_transient_error)
108+
self.assertFalse(fetch_response.is_fetched())
79109
self.assertTrue(fetch_response.entry.is_empty())
80110

81111
def test_server_side_etag(self):

configcatclienttests/test_hooks.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,32 @@ def test_evaluation(self):
118118

119119
client.close()
120120

121+
def test_callback_exception(self):
122+
with mock.patch.object(requests, 'get') as request_get:
123+
response_mock = Mock()
124+
request_get.return_value = response_mock
125+
response_mock.json.return_value = TEST_OBJECT
126+
response_mock.status_code = 200
127+
response_mock.headers = {}
128+
129+
hook_callbacks = HookCallbacks()
130+
hooks = Hooks(
131+
on_client_ready=hook_callbacks.callback_exception,
132+
on_config_changed=hook_callbacks.callback_exception,
133+
on_flag_evaluated=hook_callbacks.callback_exception,
134+
on_error=hook_callbacks.callback_exception
135+
)
136+
client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
137+
hooks=hooks))
138+
139+
client.force_refresh()
140+
141+
value = client.get_value('testStringKey', '')
142+
self.assertEqual('testValue', value)
143+
144+
value = client.get_value('', 'default')
145+
self.assertEqual('default', value)
146+
121147

122148
if __name__ == '__main__':
123149
unittest.main()

configcatclienttests/test_rollout.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def _test_matrix(self, file_path, sdk_key, type):
4444
custom_key = header.split(';')[3]
4545
content.pop(0)
4646

47-
client = configcatclient.create_client(sdk_key)
47+
client = configcatclient.get(sdk_key)
4848
errors = ''
4949
for line in content:
5050
user_descriptor = line.rstrip().split(';')
@@ -81,10 +81,10 @@ def _test_matrix(self, file_path, sdk_key, type):
8181
client.close()
8282

8383
def test_wrong_user_object(self):
84-
client = configcatclient.create_client('PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A')
84+
client = configcatclient.get('PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A')
8585
setting_value = client.get_value('stringContainsDogDefaultCat', 'Lion', {'Email': '[email protected]'})
8686
self.assertEqual('Cat', setting_value)
87-
client.close()
87+
configcatclient.close_all()
8888

8989

9090
'''

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def parse_requirements(filename):
66
return [line for line in lines if line]
77

88

9-
configcatclient_version = '6.0.3'
9+
configcatclient_version = '7.0.0'
1010

1111
requirements = parse_requirements('requirements.txt')
1212

0 commit comments

Comments
 (0)