Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix inconsistent exception type in response.json() method #5856

Merged
merged 28 commits into from Jul 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5ff9fb7
Set up json fix option 2
steveberdy Jul 2, 2021
d66d842
Fix incorrect comment in models.py
steveberdy Jul 2, 2021
a072b5e
Added support to consistently raise JSONDecodeError and ValuError, no…
steveberdy Jul 3, 2021
fc1a70f
Reduce confusion in error import naming
steveberdy Jul 3, 2021
1ac7bfe
Minor naming changes
steveberdy Jul 3, 2021
0045b83
Added check for Python 2 vs. Python 3 when importing json.JSONDecodeE…
steveberdy Jul 3, 2021
482578e
Update compat.py
steveberdy Jul 5, 2021
ac07d45
Update models.py
steveberdy Jul 5, 2021
91d9bc8
Update models.py
steveberdy Jul 5, 2021
ee45190
Update models.py
steveberdy Jul 5, 2021
66fc86e
Update models.py
steveberdy Jul 5, 2021
15bd4c5
Update quickstart.rst
steveberdy Jul 5, 2021
6705154
Update HISTORY.md
steveberdy Jul 5, 2021
9f0cbdd
Merge branch 'master' into json-opt-2
steveberdy Jul 7, 2021
12a55d8
Delete test_json.py
steveberdy Jul 7, 2021
a93d4af
Replaced json with complexjson
steveberdy Jul 8, 2021
5193ec5
Edited documentation and other minor changes
steveberdy Jul 9, 2021
3230e23
Made JSONDecodeError a subclass of RequestException and changed docs …
steveberdy Jul 9, 2021
f76f6d1
Attempted fix of method resolution order error
steveberdy Jul 12, 2021
1d6d963
Improved backwards compatibility for exceptions
steveberdy Jul 12, 2021
5c6be66
Update quickstart.rst
steveberdy Jul 12, 2021
b6b94e1
Raise JSONDecodeError without args when in Python 2 env
steveberdy Jul 12, 2021
090fe0e
Edited old GitHub API endpoint in api.rst
steveberdy Jul 12, 2021
2741955
Merge branch 'master' into json-opt-2
steveberdy Jul 13, 2021
e18e879
Raise error with message in Python 2
steveberdy Jul 14, 2021
bcfde77
Merge branch 'json-opt-2' of https://github.com/steveberdy/requests i…
steveberdy Jul 14, 2021
64b3f18
Push docs to next release
steveberdy Jul 15, 2021
8f0dcc4
Cleared confusion in exceptions.py. Added test
steveberdy Jul 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion HISTORY.md
Expand Up @@ -4,7 +4,12 @@ Release History
dev
---

- \[Short description of non-trivial change.\]
- \[Short description of non-trivial change.\]

- Added a `requests.exceptions.JSONDecodeError` to decrease inconsistencies
in the library. This gets raised in the `response.json()` method, and is
backwards compatible as it inherits from previously thrown exceptions.
Can be caught from `requests.exceptions.RequestException` as well.

2.26.0 (2021-07-13)
-------------------
Expand Down
6 changes: 3 additions & 3 deletions docs/user/quickstart.rst
Expand Up @@ -153,9 +153,9 @@ There's also a builtin JSON decoder, in case you're dealing with JSON data::

In case the JSON decoding fails, ``r.json()`` raises an exception. For example, if
the response gets a 204 (No Content), or if the response contains invalid JSON,
attempting ``r.json()`` raises ``simplejson.JSONDecodeError`` if simplejson is
installed or raises ``ValueError: No JSON object could be decoded`` on Python 2 or
``json.JSONDecodeError`` on Python 3.
attempting ``r.json()`` raises ``requests.exceptions.JSONDecodeError``. This wrapper exception
provides interoperability for multiple exceptions that may be thrown by different
python versions and json serialization libraries.

It should be noted that the success of the call to ``r.json()`` does **not**
indicate the success of the response. Some servers may return a JSON object in a
Expand Down
2 changes: 1 addition & 1 deletion requests/__init__.py
Expand Up @@ -139,7 +139,7 @@ def _check_cryptography(cryptography_version):
from .exceptions import (
RequestException, Timeout, URLRequired,
TooManyRedirects, HTTPError, ConnectionError,
FileModeWarning, ConnectTimeout, ReadTimeout
FileModeWarning, ConnectTimeout, ReadTimeout, JSONDecodeError
)

# Set default logging handler to avoid "No handler found" warnings.
Expand Down
8 changes: 7 additions & 1 deletion requests/compat.py
Expand Up @@ -28,8 +28,10 @@
#: Python 3.x?
is_py3 = (_ver[0] == 3)

has_simplejson = False
nateprewitt marked this conversation as resolved.
Show resolved Hide resolved
try:
import simplejson as json
has_simplejson = True
except ImportError:
import json

Expand All @@ -49,13 +51,13 @@
# Keep OrderedDict for backwards compatibility.
from collections import Callable, Mapping, MutableMapping, OrderedDict


builtin_str = str
bytes = str
str = unicode
basestring = basestring
numeric_types = (int, long, float)
integer_types = (int, long)
JSONDecodeError = ValueError

elif is_py3:
from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag
Expand All @@ -66,6 +68,10 @@
# Keep OrderedDict for backwards compatibility.
from collections import OrderedDict
from collections.abc import Callable, Mapping, MutableMapping
if has_simplejson:
from simplejson import JSONDecodeError
else:
from json import JSONDecodeError

builtin_str = str
str = str
Expand Down
6 changes: 6 additions & 0 deletions requests/exceptions.py
Expand Up @@ -8,6 +8,8 @@
"""
from urllib3.exceptions import HTTPError as BaseHTTPError

from .compat import JSONDecodeError as CompatJSONDecodeError


class RequestException(IOError):
"""There was an ambiguous exception that occurred while handling your
Expand All @@ -29,6 +31,10 @@ class InvalidJSONError(RequestException):
"""A JSON error occurred."""


class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError):
"""Couldn't decode the text into json"""


class HTTPError(RequestException):
"""An HTTP error occurred."""

Expand Down
29 changes: 18 additions & 11 deletions requests/models.py
Expand Up @@ -29,7 +29,9 @@
from .cookies import cookiejar_from_dict, get_cookie_header, _copy_cookie_jar
from .exceptions import (
HTTPError, MissingSchema, InvalidURL, ChunkedEncodingError,
ContentDecodingError, ConnectionError, StreamConsumedError, InvalidJSONError)
ContentDecodingError, ConnectionError, StreamConsumedError,
InvalidJSONError)
from .exceptions import JSONDecodeError as RequestsJSONDecodeError
from ._internal_utils import to_native_string, unicode_is_ascii
from .utils import (
guess_filename, get_auth_from_url, requote_uri,
Expand All @@ -38,7 +40,7 @@
from .compat import (
Callable, Mapping,
cookielib, urlunparse, urlsplit, urlencode, str, bytes,
is_py2, chardet, builtin_str, basestring)
is_py2, chardet, builtin_str, basestring, JSONDecodeError)
from .compat import json as complexjson
from .status_codes import codes

Expand Down Expand Up @@ -468,9 +470,9 @@ def prepare_body(self, data, files, json=None):
content_type = 'application/json'

try:
body = complexjson.dumps(json, allow_nan=False)
body = complexjson.dumps(json, allow_nan=False)
except ValueError as ve:
raise InvalidJSONError(ve, request=self)
raise InvalidJSONError(ve, request=self)

if not isinstance(body, bytes):
body = body.encode('utf-8')
Expand Down Expand Up @@ -882,12 +884,8 @@ def json(self, **kwargs):
r"""Returns the json-encoded content of a response, if any.

:param \*\*kwargs: Optional arguments that ``json.loads`` takes.
:raises simplejson.JSONDecodeError: If the response body does not
contain valid json and simplejson is installed.
:raises json.JSONDecodeError: If the response body does not contain
valid json and simplejson is not installed on Python 3.
:raises ValueError: If the response body does not contain valid
json and simplejson is not installed on Python 2.
:raises requests.exceptions.JSONDecodeError: If the response body does not
contain valid json.
"""

if not self.encoding and self.content and len(self.content) > 3:
Expand All @@ -907,7 +905,16 @@ def json(self, **kwargs):
# and the server didn't bother to tell us what codec *was*
# used.
pass
return complexjson.loads(self.text, **kwargs)

try:
return complexjson.loads(self.text, **kwargs)
except JSONDecodeError as e:
# Catch JSON-related errors and raise as requests.JSONDecodeError
# This aliases json.JSONDecodeError and simplejson.JSONDecodeError
if is_py2: # e is a ValueError
raise RequestsJSONDecodeError(e.message)
else:
raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)

@property
def links(self):
Expand Down
7 changes: 6 additions & 1 deletion tests/test_requests.py
Expand Up @@ -2570,4 +2570,9 @@ def test_parameters_for_nonstandard_schemes(self, input, params, expected):
def test_post_json_nan(self, httpbin):
data = {"foo": float("nan")}
with pytest.raises(requests.exceptions.InvalidJSONError):
r = requests.post(httpbin('post'), json=data)
r = requests.post(httpbin('post'), json=data)

def test_json_decode_compatibility(self, httpbin):
r = requests.get(httpbin('bytes/20'))
with pytest.raises(requests.exceptions.JSONDecodeError):
r.json()