We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
JIRAError does not correctly display full error information
There seems to be a bug in `JIRAError.__str__`

```
    if self.response and hasattr(self.response, 'text'):
        t += "\n\tresponse text = " % self.response.headers
```

https://github.com/pycontribs/jira/blob/master/jira/utils.py#L161
1. This checks for the `text` attribute but then uses the `headers` attribute (cut-and-paste error from the lines above?)
2. The response object is "Truthy" based upon the HTTP response, so in the case of a HTTP error it is actually "False" so the conditional check fails.
3. Which is just as well, because once you fix this you'll see that the format string is invalid and causes an exception because it is missing a %s at the end

:-)

I'll submit a pull request to fix this up shortly.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `jira/utils.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3 import threading
4 import json
5 import logging
6 
7 
8 class CaseInsensitiveDict(dict):
9 
10     """
11     A case-insensitive ``dict``-like object.
12 
13     Implements all methods and operations of
14     ``collections.MutableMapping`` as well as dict's ``copy``. Also
15     provides ``lower_items``.
16 
17     All keys are expected to be strings. The structure remembers the
18     case of the last key to be set, and ``iter(instance)``,
19     ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
20     will contain case-sensitive keys. However, querying and contains
21     testing is case insensitive::
22 
23         cid = CaseInsensitiveDict()
24         cid['Accept'] = 'application/json'
25         cid['aCCEPT'] == 'application/json'  # True
26         list(cid) == ['Accept']  # True
27 
28     For example, ``headers['content-encoding']`` will return the
29     value of a ``'Content-Encoding'`` response header, regardless
30     of how the header name was originally stored.
31 C
32     If the constructor, ``.update``, or equality comparison
33     operations are given keys that have equal ``.lower()``s, the
34     behavior is undefined.
35 
36     """
37 
38     def __init__(self, *args, **kw):
39         super(CaseInsensitiveDict, self).__init__(*args, **kw)
40 
41         self.itemlist = {}
42         for key, value in super(CaseInsensitiveDict, self).items():
43             if key != key.lower():
44                 self[key.lower()] = value
45                 self.pop(key, None)
46 
47         #self.itemlist[key.lower()] = value
48 
49     def __setitem__(self, key, value):
50         super(CaseInsensitiveDict, self).__setitem__(key.lower(), value)
51 
52     # def __iter__(self):
53     #    return iter(self.itemlist)
54 
55     # def keys(self):
56     #    return self.itemlist
57 
58     # def values(self):
59     #    return [self[key] for key in self]
60 
61     # def itervalues(self):
62     #    return (self[key] for key in self)
63 
64 
65 def threaded_requests(requests):
66     for fn, url, request_args in requests:
67         th = threading.Thread(
68             target=fn, args=(url,), kwargs=request_args, name=url,
69         )
70         th.start()
71 
72     for th in threading.enumerate():
73         if th.name.startswith('http'):
74             th.join()
75 
76 
77 def json_loads(r):
78     raise_on_error(r)
79     if len(r.text):  # r.status_code != 204:
80         return json.loads(r.text)
81     else:
82         # json.loads() fails with empy bodies
83         return {}
84 
85 
86 def raise_on_error(r, verb='???', **kwargs):
87     request = kwargs.get('request', None)
88     headers = kwargs.get('headers', None)
89 
90     if r is None:
91         raise JIRAError(None, **kwargs)
92 
93     if r.status_code >= 400:
94         error = ''
95         if r.status_code == 403 and "x-authentication-denied-reason" in r.headers:
96             error = r.headers["x-authentication-denied-reason"]
97         elif r.text:
98             try:
99                 response = json.loads(r.text)
100                 if 'message' in response:
101                     # JIRA 5.1 errors
102                     error = response['message']
103                 elif 'errorMessages' in response and len(response['errorMessages']) > 0:
104                     # JIRA 5.0.x error messages sometimes come wrapped in this array
105                     # Sometimes this is present but empty
106                     errorMessages = response['errorMessages']
107                     if isinstance(errorMessages, (list, tuple)):
108                         error = errorMessages[0]
109                     else:
110                         error = errorMessages
111                 elif 'errors' in response and len(response['errors']) > 0:
112                     # JIRA 6.x error messages are found in this array.
113                     error_list = response['errors'].values()
114                     error = ", ".join(error_list)
115                 else:
116                     error = r.text
117             except ValueError:
118                 error = r.text
119         raise JIRAError(
120             r.status_code, error, r.url, request=request, response=r, **kwargs)
121     # for debugging weird errors on CI
122     if r.status_code not in [200, 201, 202, 204]:
123         raise JIRAError(r.status_code, request=request, response=r, **kwargs)
124     # testing for the WTH bug exposed on
125     # https://answers.atlassian.com/questions/11457054/answers/11975162
126     if r.status_code == 200 and len(r.text) == 0 \
127             and 'X-Seraph-LoginReason' in r.headers \
128             and 'AUTHENTICATED_FAILED' in r.headers['X-Seraph-LoginReason']:
129         pass
130 
131 
132 class JIRAError(Exception):
133 
134     """General error raised for all problems in operation of the client."""
135 
136     def __init__(self, status_code=None, text=None, url=None, request=None, response=None, **kwargs):
137         self.status_code = status_code
138         self.text = text
139         self.url = url
140         self.request = request
141         self.response = response
142         self.headers = kwargs.get('headers', None)
143 
144     def __str__(self):
145         t = "JiraError HTTP %s" % self.status_code
146         if self.text:
147             t += "\n\ttext: %s" % self.text
148         if self.url:
149             t += "\n\turl: %s" % self.url
150 
151         if self.request and hasattr(self.request, 'headers'):
152             t += "\n\trequest headers = " % self.request.headers
153 
154         if self.request and hasattr(self.request, 'text'):
155             t += "\n\trequest text = " % self.request.text
156 
157         if self.response and hasattr(self.response, 'headers'):
158             t += "\n\tresponse headers = " % self.response.headers
159 
160         if self.response and hasattr(self.response, 'text'):
161             t += "\n\tresponse text = " % self.response.headers
162 
163         t += '\n'
164         return t
165 
166 
167 def get_error_list(r):
168     error_list = []
169     if r.status_code >= 400:
170         if r.status_code == 403 and "x-authentication-denied-reason" in r.headers:
171             error_list = [r.headers["x-authentication-denied-reason"]]
172         elif r.text:
173             try:
174                 response = json_loads(r)
175                 if 'message' in response:
176                     # JIRA 5.1 errors
177                     error_list = [response['message']]
178                 elif 'errorMessages' in response and len(response['errorMessages']) > 0:
179                     # JIRA 5.0.x error messages sometimes come wrapped in this array
180                     # Sometimes this is present but empty
181                     errorMessages = response['errorMessages']
182                     if isinstance(errorMessages, (list, tuple)):
183                         error_list = errorMessages
184                     else:
185                         error_list = [errorMessages]
186                 elif 'errors' in response and len(response['errors']) > 0:
187                     # JIRA 6.x error messages are found in this array.
188                     error_list = response['errors'].values()
189                 else:
190                     error_list = [r.text]
191             except ValueError:
192                 error_list = [r.text]
193     return error_list
194
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/jira/utils.py b/jira/utils.py
--- a/jira/utils.py
+++ b/jira/utils.py
@@ -148,17 +148,17 @@
         if self.url:
             t += "\n\turl: %s" % self.url
 
-        if self.request and hasattr(self.request, 'headers'):
-            t += "\n\trequest headers = " % self.request.headers
+        if self.request is not None and hasattr(self.request, 'headers'):
+            t += "\n\trequest headers = %s" % self.request.headers
 
-        if self.request and hasattr(self.request, 'text'):
-            t += "\n\trequest text = " % self.request.text
+        if self.request is not None and hasattr(self.request, 'text'):
+            t += "\n\trequest text = %s" % self.request.text
 
-        if self.response and hasattr(self.response, 'headers'):
-            t += "\n\tresponse headers = " % self.response.headers
+        if self.response is not None and hasattr(self.response, 'headers'):
+            t += "\n\tresponse headers = %s" % self.response.headers
 
-        if self.response and hasattr(self.response, 'text'):
-            t += "\n\tresponse text = " % self.response.headers
+        if self.response is not None and hasattr(self.response, 'text'):
+            t += "\n\tresponse text = %s" % self.response.text
 
         t += '\n'
         return t

{"golden_diff": "diff --git a/jira/utils.py b/jira/utils.py\n--- a/jira/utils.py\n+++ b/jira/utils.py\n@@ -148,17 +148,17 @@\n         if self.url:\n             t += \"\\n\\turl: %s\" % self.url\n \n-        if self.request and hasattr(self.request, 'headers'):\n-            t += \"\\n\\trequest headers = \" % self.request.headers\n+        if self.request is not None and hasattr(self.request, 'headers'):\n+            t += \"\\n\\trequest headers = %s\" % self.request.headers\n \n-        if self.request and hasattr(self.request, 'text'):\n-            t += \"\\n\\trequest text = \" % self.request.text\n+        if self.request is not None and hasattr(self.request, 'text'):\n+            t += \"\\n\\trequest text = %s\" % self.request.text\n \n-        if self.response and hasattr(self.response, 'headers'):\n-            t += \"\\n\\tresponse headers = \" % self.response.headers\n+        if self.response is not None and hasattr(self.response, 'headers'):\n+            t += \"\\n\\tresponse headers = %s\" % self.response.headers\n \n-        if self.response and hasattr(self.response, 'text'):\n-            t += \"\\n\\tresponse text = \" % self.response.headers\n+        if self.response is not None and hasattr(self.response, 'text'):\n+            t += \"\\n\\tresponse text = %s\" % self.response.text\n \n         t += '\\n'\n         return t\n", "issue": "JIRAError does not correctly display full error information\nThere seems to be a bug in `JIRAError.__str__`\n\n```\n    if self.response and hasattr(self.response, 'text'):\n        t += \"\\n\\tresponse text = \" % self.response.headers\n```\n\nhttps://github.com/pycontribs/jira/blob/master/jira/utils.py#L161\n1. This checks for the `text` attribute but then uses the `headers` attribute (cut-and-paste error from the lines above?)\n2. The response object is \"Truthy\" based upon the HTTP response, so in the case of a HTTP error it is actually \"False\" so the conditional check fails.\n3. Which is just as well, because once you fix this you'll see that the format string is invalid and causes an exception because it is missing a %s at the end\n\n:-)\n\nI'll submit a pull request to fix this up shortly.\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\nimport threading\nimport json\nimport logging\n\n\nclass CaseInsensitiveDict(dict):\n\n    \"\"\"\n    A case-insensitive ``dict``-like object.\n\n    Implements all methods and operations of\n    ``collections.MutableMapping`` as well as dict's ``copy``. Also\n    provides ``lower_items``.\n\n    All keys are expected to be strings. The structure remembers the\n    case of the last key to be set, and ``iter(instance)``,\n    ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``\n    will contain case-sensitive keys. However, querying and contains\n    testing is case insensitive::\n\n        cid = CaseInsensitiveDict()\n        cid['Accept'] = 'application/json'\n        cid['aCCEPT'] == 'application/json'  # True\n        list(cid) == ['Accept']  # True\n\n    For example, ``headers['content-encoding']`` will return the\n    value of a ``'Content-Encoding'`` response header, regardless\n    of how the header name was originally stored.\nC\n    If the constructor, ``.update``, or equality comparison\n    operations are given keys that have equal ``.lower()``s, the\n    behavior is undefined.\n\n    \"\"\"\n\n    def __init__(self, *args, **kw):\n        super(CaseInsensitiveDict, self).__init__(*args, **kw)\n\n        self.itemlist = {}\n        for key, value in super(CaseInsensitiveDict, self).items():\n            if key != key.lower():\n                self[key.lower()] = value\n                self.pop(key, None)\n\n        #self.itemlist[key.lower()] = value\n\n    def __setitem__(self, key, value):\n        super(CaseInsensitiveDict, self).__setitem__(key.lower(), value)\n\n    # def __iter__(self):\n    #    return iter(self.itemlist)\n\n    # def keys(self):\n    #    return self.itemlist\n\n    # def values(self):\n    #    return [self[key] for key in self]\n\n    # def itervalues(self):\n    #    return (self[key] for key in self)\n\n\ndef threaded_requests(requests):\n    for fn, url, request_args in requests:\n        th = threading.Thread(\n            target=fn, args=(url,), kwargs=request_args, name=url,\n        )\n        th.start()\n\n    for th in threading.enumerate():\n        if th.name.startswith('http'):\n            th.join()\n\n\ndef json_loads(r):\n    raise_on_error(r)\n    if len(r.text):  # r.status_code != 204:\n        return json.loads(r.text)\n    else:\n        # json.loads() fails with empy bodies\n        return {}\n\n\ndef raise_on_error(r, verb='???', **kwargs):\n    request = kwargs.get('request', None)\n    headers = kwargs.get('headers', None)\n\n    if r is None:\n        raise JIRAError(None, **kwargs)\n\n    if r.status_code >= 400:\n        error = ''\n        if r.status_code == 403 and \"x-authentication-denied-reason\" in r.headers:\n            error = r.headers[\"x-authentication-denied-reason\"]\n        elif r.text:\n            try:\n                response = json.loads(r.text)\n                if 'message' in response:\n                    # JIRA 5.1 errors\n                    error = response['message']\n                elif 'errorMessages' in response and len(response['errorMessages']) > 0:\n                    # JIRA 5.0.x error messages sometimes come wrapped in this array\n                    # Sometimes this is present but empty\n                    errorMessages = response['errorMessages']\n                    if isinstance(errorMessages, (list, tuple)):\n                        error = errorMessages[0]\n                    else:\n                        error = errorMessages\n                elif 'errors' in response and len(response['errors']) > 0:\n                    # JIRA 6.x error messages are found in this array.\n                    error_list = response['errors'].values()\n                    error = \", \".join(error_list)\n                else:\n                    error = r.text\n            except ValueError:\n                error = r.text\n        raise JIRAError(\n            r.status_code, error, r.url, request=request, response=r, **kwargs)\n    # for debugging weird errors on CI\n    if r.status_code not in [200, 201, 202, 204]:\n        raise JIRAError(r.status_code, request=request, response=r, **kwargs)\n    # testing for the WTH bug exposed on\n    # https://answers.atlassian.com/questions/11457054/answers/11975162\n    if r.status_code == 200 and len(r.text) == 0 \\\n            and 'X-Seraph-LoginReason' in r.headers \\\n            and 'AUTHENTICATED_FAILED' in r.headers['X-Seraph-LoginReason']:\n        pass\n\n\nclass JIRAError(Exception):\n\n    \"\"\"General error raised for all problems in operation of the client.\"\"\"\n\n    def __init__(self, status_code=None, text=None, url=None, request=None, response=None, **kwargs):\n        self.status_code = status_code\n        self.text = text\n        self.url = url\n        self.request = request\n        self.response = response\n        self.headers = kwargs.get('headers', None)\n\n    def __str__(self):\n        t = \"JiraError HTTP %s\" % self.status_code\n        if self.text:\n            t += \"\\n\\ttext: %s\" % self.text\n        if self.url:\n            t += \"\\n\\turl: %s\" % self.url\n\n        if self.request and hasattr(self.request, 'headers'):\n            t += \"\\n\\trequest headers = \" % self.request.headers\n\n        if self.request and hasattr(self.request, 'text'):\n            t += \"\\n\\trequest text = \" % self.request.text\n\n        if self.response and hasattr(self.response, 'headers'):\n            t += \"\\n\\tresponse headers = \" % self.response.headers\n\n        if self.response and hasattr(self.response, 'text'):\n            t += \"\\n\\tresponse text = \" % self.response.headers\n\n        t += '\\n'\n        return t\n\n\ndef get_error_list(r):\n    error_list = []\n    if r.status_code >= 400:\n        if r.status_code == 403 and \"x-authentication-denied-reason\" in r.headers:\n            error_list = [r.headers[\"x-authentication-denied-reason\"]]\n        elif r.text:\n            try:\n                response = json_loads(r)\n                if 'message' in response:\n                    # JIRA 5.1 errors\n                    error_list = [response['message']]\n                elif 'errorMessages' in response and len(response['errorMessages']) > 0:\n                    # JIRA 5.0.x error messages sometimes come wrapped in this array\n                    # Sometimes this is present but empty\n                    errorMessages = response['errorMessages']\n                    if isinstance(errorMessages, (list, tuple)):\n                        error_list = errorMessages\n                    else:\n                        error_list = [errorMessages]\n                elif 'errors' in response and len(response['errors']) > 0:\n                    # JIRA 6.x error messages are found in this array.\n                    error_list = response['errors'].values()\n                else:\n                    error_list = [r.text]\n            except ValueError:\n                error_list = [r.text]\n    return error_list\n", "path": "jira/utils.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nfrom __future__ import unicode_literals\nimport threading\nimport json\nimport logging\n\n\nclass CaseInsensitiveDict(dict):\n\n    \"\"\"\n    A case-insensitive ``dict``-like object.\n\n    Implements all methods and operations of\n    ``collections.MutableMapping`` as well as dict's ``copy``. Also\n    provides ``lower_items``.\n\n    All keys are expected to be strings. The structure remembers the\n    case of the last key to be set, and ``iter(instance)``,\n    ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``\n    will contain case-sensitive keys. However, querying and contains\n    testing is case insensitive::\n\n        cid = CaseInsensitiveDict()\n        cid['Accept'] = 'application/json'\n        cid['aCCEPT'] == 'application/json'  # True\n        list(cid) == ['Accept']  # True\n\n    For example, ``headers['content-encoding']`` will return the\n    value of a ``'Content-Encoding'`` response header, regardless\n    of how the header name was originally stored.\nC\n    If the constructor, ``.update``, or equality comparison\n    operations are given keys that have equal ``.lower()``s, the\n    behavior is undefined.\n\n    \"\"\"\n\n    def __init__(self, *args, **kw):\n        super(CaseInsensitiveDict, self).__init__(*args, **kw)\n\n        self.itemlist = {}\n        for key, value in super(CaseInsensitiveDict, self).items():\n            if key != key.lower():\n                self[key.lower()] = value\n                self.pop(key, None)\n\n        #self.itemlist[key.lower()] = value\n\n    def __setitem__(self, key, value):\n        super(CaseInsensitiveDict, self).__setitem__(key.lower(), value)\n\n    # def __iter__(self):\n    #    return iter(self.itemlist)\n\n    # def keys(self):\n    #    return self.itemlist\n\n    # def values(self):\n    #    return [self[key] for key in self]\n\n    # def itervalues(self):\n    #    return (self[key] for key in self)\n\n\ndef threaded_requests(requests):\n    for fn, url, request_args in requests:\n        th = threading.Thread(\n            target=fn, args=(url,), kwargs=request_args, name=url,\n        )\n        th.start()\n\n    for th in threading.enumerate():\n        if th.name.startswith('http'):\n            th.join()\n\n\ndef json_loads(r):\n    raise_on_error(r)\n    if len(r.text):  # r.status_code != 204:\n        return json.loads(r.text)\n    else:\n        # json.loads() fails with empy bodies\n        return {}\n\n\ndef raise_on_error(r, verb='???', **kwargs):\n    request = kwargs.get('request', None)\n    headers = kwargs.get('headers', None)\n\n    if r is None:\n        raise JIRAError(None, **kwargs)\n\n    if r.status_code >= 400:\n        error = ''\n        if r.status_code == 403 and \"x-authentication-denied-reason\" in r.headers:\n            error = r.headers[\"x-authentication-denied-reason\"]\n        elif r.text:\n            try:\n                response = json.loads(r.text)\n                if 'message' in response:\n                    # JIRA 5.1 errors\n                    error = response['message']\n                elif 'errorMessages' in response and len(response['errorMessages']) > 0:\n                    # JIRA 5.0.x error messages sometimes come wrapped in this array\n                    # Sometimes this is present but empty\n                    errorMessages = response['errorMessages']\n                    if isinstance(errorMessages, (list, tuple)):\n                        error = errorMessages[0]\n                    else:\n                        error = errorMessages\n                elif 'errors' in response and len(response['errors']) > 0:\n                    # JIRA 6.x error messages are found in this array.\n                    error_list = response['errors'].values()\n                    error = \", \".join(error_list)\n                else:\n                    error = r.text\n            except ValueError:\n                error = r.text\n        raise JIRAError(\n            r.status_code, error, r.url, request=request, response=r, **kwargs)\n    # for debugging weird errors on CI\n    if r.status_code not in [200, 201, 202, 204]:\n        raise JIRAError(r.status_code, request=request, response=r, **kwargs)\n    # testing for the WTH bug exposed on\n    # https://answers.atlassian.com/questions/11457054/answers/11975162\n    if r.status_code == 200 and len(r.text) == 0 \\\n            and 'X-Seraph-LoginReason' in r.headers \\\n            and 'AUTHENTICATED_FAILED' in r.headers['X-Seraph-LoginReason']:\n        pass\n\n\nclass JIRAError(Exception):\n\n    \"\"\"General error raised for all problems in operation of the client.\"\"\"\n\n    def __init__(self, status_code=None, text=None, url=None, request=None, response=None, **kwargs):\n        self.status_code = status_code\n        self.text = text\n        self.url = url\n        self.request = request\n        self.response = response\n        self.headers = kwargs.get('headers', None)\n\n    def __str__(self):\n        t = \"JiraError HTTP %s\" % self.status_code\n        if self.text:\n            t += \"\\n\\ttext: %s\" % self.text\n        if self.url:\n            t += \"\\n\\turl: %s\" % self.url\n\n        if self.request is not None and hasattr(self.request, 'headers'):\n            t += \"\\n\\trequest headers = %s\" % self.request.headers\n\n        if self.request is not None and hasattr(self.request, 'text'):\n            t += \"\\n\\trequest text = %s\" % self.request.text\n\n        if self.response is not None and hasattr(self.response, 'headers'):\n            t += \"\\n\\tresponse headers = %s\" % self.response.headers\n\n        if self.response is not None and hasattr(self.response, 'text'):\n            t += \"\\n\\tresponse text = %s\" % self.response.text\n\n        t += '\\n'\n        return t\n\n\ndef get_error_list(r):\n    error_list = []\n    if r.status_code >= 400:\n        if r.status_code == 403 and \"x-authentication-denied-reason\" in r.headers:\n            error_list = [r.headers[\"x-authentication-denied-reason\"]]\n        elif r.text:\n            try:\n                response = json_loads(r)\n                if 'message' in response:\n                    # JIRA 5.1 errors\n                    error_list = [response['message']]\n                elif 'errorMessages' in response and len(response['errorMessages']) > 0:\n                    # JIRA 5.0.x error messages sometimes come wrapped in this array\n                    # Sometimes this is present but empty\n                    errorMessages = response['errorMessages']\n                    if isinstance(errorMessages, (list, tuple)):\n                        error_list = errorMessages\n                    else:\n                        error_list = [errorMessages]\n                elif 'errors' in response and len(response['errors']) > 0:\n                    # JIRA 6.x error messages are found in this array.\n                    error_list = response['errors'].values()\n                else:\n                    error_list = [r.text]\n            except ValueError:\n                error_list = [r.text]\n    return error_list\n", "path": "jira/utils.py"}]}