Repository: certbot/certbot

Issue:
Produce sensible error states
Often, we'll just crash with an unhelpful traceback, rather than telling the user what went wrong in a friendly manner.  For instance, if running the <tt>boulder</tt> apache client from behind a firewall, I get this crash:

```
Traceback (most recent call last):
  File "/usr/local/bin/letsencrypt", line 9, in <module>
    load_entry_point('letsencrypt==0.1', 'console_scripts', 'letsencrypt')()
  File "/usr/local/lib/python2.7/dist-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/scripts/main.py", line 214, in main
    cert_file, chain_file = acme.obtain_certificate(doms)
  File "/usr/local/lib/python2.7/dist-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/client.py", line 134, in obtain_certificate
    domains, self.regr.new_authzr_uri)
  File "/usr/local/lib/python2.7/dist-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/auth_handler.py", line 69, in get_authorizations
    domain, new_authz_uri)
  File "/usr/local/lib/python2.7/dist-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/network2.py", line 279, in request_domain_challenges
    typ=messages2.IDENTIFIER_FQDN, value=domain), new_authz_uri)
  File "/usr/local/lib/python2.7/dist-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/network2.py", line 260, in request_challenges
    response = self._post(new_authzr_uri, self._wrap_in_jws(new_authz))
  File "/usr/local/lib/python2.7/dist-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/network2.py", line 143, in _post
    self._check_response(response, content_type=content_type)
  File "/usr/local/lib/python2.7/dist-packages/letsencrypt-0.1-py2.7.egg/letsencrypt/client/network2.py", line 92, in _check_response
    raise messages2.Error.from_json(jobj)
letsencrypt.acme.messages2.Error
```



Discussion:
These error cases should extract a reason, and call out to the display plugin to present it.


Resolving patch:
diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py
--- a/letsencrypt/acme/messages2.py
+++ b/letsencrypt/acme/messages2.py
@@ -46,6 +46,11 @@ def description(self):
         """Hardcoded error description based on its type."""
         return self.ERROR_TYPE_DESCRIPTIONS[self.typ]
 
+    def __str__(self):
+        if self.typ is not None:
+            return ' :: '.join([self.typ, self.description, self.detail])
+        else:
+            return str(self.detail)
 
 class _Constant(jose.JSONDeSerializable):
     """ACME constant."""


Test patch:
diff --git a/letsencrypt/acme/messages2_test.py b/letsencrypt/acme/messages2_test.py
--- a/letsencrypt/acme/messages2_test.py
+++ b/letsencrypt/acme/messages2_test.py
@@ -50,6 +50,12 @@ def test_from_json_hashable(self):
         from letsencrypt.acme.messages2 import Error
         hash(Error.from_json(self.error.to_json()))
 
+    def test_str(self):
+        self.assertEqual(
+            'malformed :: The request message was malformed :: foo',
+            str(self.error))
+        self.assertEqual('foo', str(self.error.update(typ=None)))
+
 
 class ConstantTest(unittest.TestCase):
     """Tests for letsencrypt.acme.messages2._Constant."""
@@ -163,6 +169,9 @@ def test_from_json_hashable(self):
         from letsencrypt.acme.messages2 import ChallengeBody
         hash(ChallengeBody.from_json(self.jobj_from))
 
+    def test_proxy(self):
+        self.assertEqual('foo', self.challb.token)
+
 
 class AuthorizationTest(unittest.TestCase):
     """Tests for letsencrypt.acme.messages2.Authorization."""