Skip to content

Commit 18ab557

Browse files
benweissmannaliabbasrizvi
authored andcommitted
fix(exception-handling): Fix handling of network and other non-status-code errors when polling for datafile (#287)
1 parent e66f441 commit 18ab557

File tree

3 files changed

+113
-9
lines changed

3 files changed

+113
-9
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ Build and install the SDK with pip, using the following command:
176176
To get test dependencies installed, use a modified version of the
177177
install command:
178178

179-
pip install -e .[test]
179+
pip install -e '.[test]'
180180

181181
You can run all unit tests with:
182182

optimizely/config_manager.py

+16-6
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,14 @@ def fetch_datafile(self):
344344
if self.last_modified:
345345
request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified
346346

347-
response = requests.get(
348-
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
349-
)
347+
try:
348+
response = requests.get(
349+
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
350+
)
351+
except requests_exceptions.RequestException as err:
352+
self.logger.error('Fetching datafile from {} failed. Error: {}'.format(self.datafile_url, str(err)))
353+
return
354+
350355
self._handle_response(response)
351356

352357
@property
@@ -411,7 +416,12 @@ def fetch_datafile(self):
411416
if self.last_modified:
412417
request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified
413418

414-
response = requests.get(
415-
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
416-
)
419+
try:
420+
response = requests.get(
421+
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
422+
)
423+
except requests_exceptions.RequestException as err:
424+
self.logger.error('Fetching datafile from {} failed. Error: {}'.format(self.datafile_url, str(err)))
425+
return
426+
417427
self._handle_response(response)

tests/test_config_manager.py

+96-2
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,8 @@ def test_fetch_datafile(self, _):
394394
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
395395
self.assertTrue(project_config_manager.is_running)
396396

397-
def test_fetch_datafile__exception_raised(self, _):
398-
""" Test that config_manager keeps running if exception is raised when fetching datafile. """
397+
def test_fetch_datafile__status_exception_raised(self, _):
398+
""" Test that config_manager keeps running if status code exception is raised when fetching datafile. """
399399
class MockExceptionResponse(object):
400400
def raise_for_status(self):
401401
raise requests.exceptions.RequestException('Error Error !!')
@@ -434,6 +434,45 @@ def raise_for_status(self):
434434
# Confirm that config manager keeps running
435435
self.assertTrue(project_config_manager.is_running)
436436

437+
def test_fetch_datafile__request_exception_raised(self, _):
438+
""" Test that config_manager keeps running if a request exception is raised when fetching datafile. """
439+
sdk_key = 'some_key'
440+
mock_logger = mock.Mock()
441+
with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'):
442+
project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key, logger=mock_logger)
443+
expected_datafile_url = enums.ConfigManager.DATAFILE_URL_TEMPLATE.format(sdk_key=sdk_key)
444+
test_headers = {'Last-Modified': 'New Time'}
445+
test_datafile = json.dumps(self.config_dict_with_features)
446+
test_response = requests.Response()
447+
test_response.status_code = 200
448+
test_response.headers = test_headers
449+
test_response._content = test_datafile
450+
with mock.patch('requests.get', return_value=test_response):
451+
project_config_manager.fetch_datafile()
452+
453+
self.assertEqual(test_headers['Last-Modified'], project_config_manager.last_modified)
454+
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
455+
456+
# Call fetch_datafile again, but raise exception this time
457+
with mock.patch(
458+
'requests.get',
459+
side_effect=requests.exceptions.RequestException('Error Error !!'),
460+
) as mock_requests:
461+
project_config_manager.fetch_datafile()
462+
463+
mock_requests.assert_called_once_with(
464+
expected_datafile_url,
465+
headers={'If-Modified-Since': test_headers['Last-Modified']},
466+
timeout=enums.ConfigManager.REQUEST_TIMEOUT,
467+
)
468+
mock_logger.error.assert_called_once_with('Fetching datafile from {} failed. Error: Error Error !!'.format(
469+
expected_datafile_url
470+
))
471+
self.assertEqual(test_headers['Last-Modified'], project_config_manager.last_modified)
472+
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
473+
# Confirm that config manager keeps running
474+
self.assertTrue(project_config_manager.is_running)
475+
437476
def test_is_running(self, _):
438477
""" Test that polling thread is running after instance of PollingConfigManager is created. """
439478
with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'):
@@ -492,3 +531,58 @@ def test_fetch_datafile(self, _):
492531
)
493532

494533
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
534+
535+
def test_fetch_datafile__request_exception_raised(self, _):
536+
""" Test that config_manager keeps running if a request exception is raised when fetching datafile. """
537+
datafile_access_token = 'some_token'
538+
sdk_key = 'some_key'
539+
mock_logger = mock.Mock()
540+
541+
with mock.patch('optimizely.config_manager.AuthDatafilePollingConfigManager.fetch_datafile'):
542+
project_config_manager = config_manager.AuthDatafilePollingConfigManager(
543+
datafile_access_token=datafile_access_token, sdk_key=sdk_key, logger=mock_logger)
544+
expected_datafile_url = enums.ConfigManager.AUTHENTICATED_DATAFILE_URL_TEMPLATE.format(sdk_key=sdk_key)
545+
test_headers = {'Last-Modified': 'New Time'}
546+
test_datafile = json.dumps(self.config_dict_with_features)
547+
test_response = requests.Response()
548+
test_response.status_code = 200
549+
test_response.headers = test_headers
550+
test_response._content = test_datafile
551+
552+
# Call fetch_datafile and assert that request was sent with correct authorization header
553+
with mock.patch('requests.get',
554+
return_value=test_response) as mock_request:
555+
project_config_manager.fetch_datafile()
556+
557+
mock_request.assert_called_once_with(
558+
expected_datafile_url,
559+
headers={'Authorization': 'Bearer {datafile_access_token}'.format(
560+
datafile_access_token=datafile_access_token)},
561+
timeout=enums.ConfigManager.REQUEST_TIMEOUT,
562+
)
563+
564+
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
565+
566+
# Call fetch_datafile again, but raise exception this time
567+
with mock.patch(
568+
'requests.get',
569+
side_effect=requests.exceptions.RequestException('Error Error !!'),
570+
) as mock_requests:
571+
project_config_manager.fetch_datafile()
572+
573+
mock_requests.assert_called_once_with(
574+
expected_datafile_url,
575+
headers={
576+
'If-Modified-Since': test_headers['Last-Modified'],
577+
'Authorization': 'Bearer {datafile_access_token}'.format(
578+
datafile_access_token=datafile_access_token),
579+
},
580+
timeout=enums.ConfigManager.REQUEST_TIMEOUT,
581+
)
582+
mock_logger.error.assert_called_once_with('Fetching datafile from {} failed. Error: Error Error !!'.format(
583+
expected_datafile_url
584+
))
585+
self.assertEqual(test_headers['Last-Modified'], project_config_manager.last_modified)
586+
self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
587+
# Confirm that config manager keeps running
588+
self.assertTrue(project_config_manager.is_running)

0 commit comments

Comments
 (0)