Repository: certbot/certbot

Issue:
Certbot crashes if EFF e-mail submission endpoint is unavailable
Certbot will crash if the user's e-mail information can't be submitted to the EFF endpoint:

https://community.letsencrypt.org/t/issue-when-trying-to-create-certificate/59307/1

https://github.com/certbot/certbot/blob/master/certbot/eff.py#L74

It crashes with a somewhat relevant error if the endpoint is unavailable, but a confusing error if the endpoint returns something other than JSON. It should probably not crash at all in either case. If the endpoint specifically says that the user's e-mail address is malformed, that's probably worth stopping to report to the user, but if the endpoint simply doesn't reply usefully, it's probably preferable to continue since I wouldn't think we really want to make users wait in this situation.

(Adding the "bug" tag at least for the case where the endpoint returns a non-JSON reply because the error shown to the user in this case is unhelpful.)


Discussion:
I'm working on this.

Resolving patch:
diff --git a/certbot/eff.py b/certbot/eff.py
--- a/certbot/eff.py
+++ b/certbot/eff.py
@@ -71,11 +71,14 @@ def _check_response(response):
 
     """
     logger.debug('Received response:\n%s', response.content)
-    if response.ok:
-        if not response.json()['status']:
+    try:
+        response.raise_for_status()
+        if response.json()['status'] == False:
             _report_failure('your e-mail address appears to be invalid')
-    else:
+    except requests.exceptions.HTTPError:
         _report_failure()
+    except (ValueError, KeyError):
+        _report_failure('there was a problem with the server response')
 
 
 def _report_failure(reason=None):


Test patch:
diff --git a/certbot/tests/eff_test.py b/certbot/tests/eff_test.py
--- a/certbot/tests/eff_test.py
+++ b/certbot/tests/eff_test.py
@@ -1,4 +1,5 @@
 """Tests for certbot.eff."""
+import requests
 import unittest
 
 import mock
@@ -118,11 +119,28 @@ def test_bad_status(self, mock_get_utility):
     @test_util.patch_get_utility()
     def test_not_ok(self, mock_get_utility):
         self.response.ok = False
+        self.response.raise_for_status.side_effect = requests.exceptions.HTTPError
         self._call()  # pylint: disable=no-value-for-parameter
         actual = self._get_reported_message(mock_get_utility)
         unexpected_part = 'because'
         self.assertFalse(unexpected_part in actual)
 
+    @test_util.patch_get_utility()
+    def test_response_not_json(self, mock_get_utility):
+        self.response.json.side_effect = ValueError()
+        self._call()  # pylint: disable=no-value-for-parameter
+        actual = self._get_reported_message(mock_get_utility)
+        expected_part = 'problem'
+        self.assertTrue(expected_part in actual)
+
+    @test_util.patch_get_utility()
+    def test_response_json_missing_status_element(self, mock_get_utility):
+        self.json.clear()
+        self._call()  # pylint: disable=no-value-for-parameter
+        actual = self._get_reported_message(mock_get_utility)
+        expected_part = 'problem'
+        self.assertTrue(expected_part in actual)
+
     def _get_reported_message(self, mock_get_utility):
         self.assertTrue(mock_get_utility().add_message.called)
         return mock_get_utility().add_message.call_args[0][0]