Request with Empty Content Throws Errors When `.json()` is Called

I'm running into an issue where calling `.json()` on a response with empty content is resulting in JSON errors. 

## Expected Result

Calling `response.json()` results in creating some object that can be used appropriately by other functions.

## Actual Result

Calling `response.json()` results in a `KeyError`.

## Reproduction Steps

```python

session = requests.Session()
response = self.session.delete(
            "{}{}".format(self.url, "api/delete_entity/123"), 
            data=json.dumps(valid_data),
            headers=self.headers
        )
return response.json()
```

The response above looks like this:
```python
response = {Response} <Response [204]>
 _content = {bytes} b''
 _content_consumed = {bool} True
 apparent_encoding = {NoneType} None
 connection = {HTTPAdapter} <requests.adapters.HTTPAdapter object at 0x104b71eb8>
 content = {bytes} b''
 cookies = {RequestsCookieJar} <RequestsCookieJar[<Cookie sessionid=XXX for 10.8.8.8/>]>
 elapsed = {timedelta} 0:00:00.080925
 encoding = {NoneType} None
 headers = {CaseInsensitiveDict} {'Content-Length': '0', 'Vary': 'Accept, Cookie', 'Allow': 'GET, PUT, PATCH, DELETE, HEAD, OPTIONS', 'Server': 'nginx/1.4.6 (Ubuntu)', 'Connection': 'keep-alive', 'Set-Cookie': 'sessionid=XXX; expires=Wed, 05-Jul-2017 02:54:47 
 history = {list} <class 'list'>: []
 is_permanent_redirect = {bool} False
 is_redirect = {bool} False
 links = {dict} {}
 ok = {bool} True
 raw = {HTTPResponse} <requests.packages.urllib3.response.HTTPResponse object at 0x104b632b0>
 reason = {str} 'No Content'
 request = {PreparedRequest} <PreparedRequest [DELETE]>
 status_code = {int} 204
 text = {str} ''
 url = {str} 'http://10.8.8.8:8888/api/delete_entity/123'
```

## System Information

```
OS: MacOS Sierra 10.12.5
Python: 3.4.3 and Python 3.6.1
Requests: 2.18.1
```

Requests 2.13 is very old. This problem should be resolved by updating Requests.

This also occurrs using requests 2.18.1. I'll update the issue.

Can you provide the full traceback please?

Here's a traceback from a pytest run. Some values have been replaced with ### for security reasons (but those ###'s do represent valid values).

-----------------------------

```
self = <json.decoder.JSONDecoder object at 0x101f9b6d8>, s = '', idx = 0

    def raw_decode(self, s, idx=0):
        """Decode a JSON document from ``s`` (a ``str`` beginning with
            a JSON document) and return a 2-tuple of the Python
            representation and the index in ``s`` where the document ended.
    
            This can be used to decode a JSON document from a string that may
            have extraneous data at the end.
    
            """
        try:
>           obj, end = self.scan_once(s, idx)
E           StopIteration: 0

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/decoder.py:355: StopIteration

During handling of the above exception, another exception occurred:

self = <test_create_entity.Test object at 0x10a4cf4a8>
data = <data.nectar_data.Data object at 0x103c36d30>

    def test_delete_impression_flight(self, data):
        """Verify that an admin user can delete a flight from an impressions-based campaign."""
        line = data.make(LineData)
        line_resp = self.nectar.create_line(line)
        line_id = line_resp['id']
        self.nectar.line_action(line_id, "submit", line)
        assert line_resp['status_code'] == 201
    
        flight = FlightData().make(line=line_resp['id'])
        flight_resp = self.nectar.create_flight(line_id)
        assert flight_resp['status_code'] == 201
    
>       del_flight = self.nectar.delete_flight(flight_resp['id'], flight)

test_create_flight.py:109: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <apiobjects.nectar_api.NectarAPI object at 0x103cb5b70>
flight_id = ###, data = {'line': ###}

    def delete_flight(self, flight_id, data):
>       return self.delete("api/v2/flight/{}".format(flight_id), data)

../apiobjects/nectar_api.py:43: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <apiobjects.nectar_api.NectarAPI object at 0x103cb5b70>
api_call = 'api/v2/flight/###', data = {'line': ###}

    def delete(self, api_call, data):
        response = self.session.delete(
            "{}{}".format(self.url, api_call),
            data=json.dumps(data),
            headers=self.headers
        )
>       return APIResponse(response)

../apiobjects/base_api.py:53: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = {}, response = <Response [204]>

    def __init__(self, response):
        self._response = response
>       super().__init__(response.json())

../apiobjects/base_api.py:11: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [204]>, kwargs = {}

    def json(self, **kwargs):
        r"""Returns the json-encoded content of a response, if any.
    
            :param \*\*kwargs: Optional arguments that ``json.loads`` takes.
            :raises ValueError: If the response body does not contain valid json.
            """
    
        if not self.encoding and self.content and len(self.content) > 3:
            # No encoding set. JSON RFC 4627 section 3 states we should expect
            # UTF-8, -16 or -32. Detect which one to use; If the detection or
            # decoding fails, fall back to `self.text` (using chardet to make
            # a best guess).
            encoding = guess_json_utf(self.content)
            if encoding is not None:
                try:
                    return complexjson.loads(
                        self.content.decode(encoding), **kwargs
                    )
                except UnicodeDecodeError:
                    # Wrong UTF codec detected; usually because it's not UTF-8
                    # but some other 8-bit codec.  This is an RFC violation,
                    # and the server didn't bother to tell us what codec *was*
                    # used.
                    pass
>       return complexjson.loads(self.text, **kwargs)

../.venv/lib/python3.6/site-packages/requests/models.py:894: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

s = '', encoding = None, cls = None, object_hook = None, parse_float = None
parse_int = None, parse_constant = None, object_pairs_hook = None, kw = {}

    def loads(s, *, encoding=None, cls=None, object_hook=None, parse_float=None,
            parse_int=None, parse_constant=None, object_pairs_hook=None, **kw):
        """Deserialize ``s`` (a ``str``, ``bytes`` or ``bytearray`` instance
        containing a JSON document) to a Python object.
    
        ``object_hook`` is an optional function that will be called with the
        result of any object literal decode (a ``dict``). The return value of
        ``object_hook`` will be used instead of the ``dict``. This feature
        can be used to implement custom decoders (e.g. JSON-RPC class hinting).
    
        ``object_pairs_hook`` is an optional function that will be called with the
        result of any object literal decoded with an ordered list of pairs.  The
        return value of ``object_pairs_hook`` will be used instead of the ``dict``.
        This feature can be used to implement custom decoders that rely on the
        order that the key and value pairs are decoded (for example,
        collections.OrderedDict will remember the order of insertion). If
        ``object_hook`` is also defined, the ``object_pairs_hook`` takes priority.
    
        ``parse_float``, if specified, will be called with the string
        of every JSON float to be decoded. By default this is equivalent to
        float(num_str). This can be used to use another datatype or parser
        for JSON floats (e.g. decimal.Decimal).
    
        ``parse_int``, if specified, will be called with the string
        of every JSON int to be decoded. By default this is equivalent to
        int(num_str). This can be used to use another datatype or parser
        for JSON integers (e.g. float).
    
        ``parse_constant``, if specified, will be called with one of the
        following strings: -Infinity, Infinity, NaN.
        This can be used to raise an exception if invalid JSON numbers
        are encountered.
    
        To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
        kwarg; otherwise ``JSONDecoder`` is used.
    
        The ``encoding`` argument is ignored and deprecated.
    
        """
        if isinstance(s, str):
            if s.startswith('\ufeff'):
                raise JSONDecodeError("Unexpected UTF-8 BOM (decode using utf-8-sig)",
                                      s, 0)
        else:
            if not isinstance(s, (bytes, bytearray)):
                raise TypeError('the JSON object must be str, bytes or bytearray, '
                                'not {!r}'.format(s.__class__.__name__))
            s = s.decode(detect_encoding(s), 'surrogatepass')
    
        if (cls is None and object_hook is None and
                parse_int is None and parse_float is None and
                parse_constant is None and object_pairs_hook is None and not kw):
>           return _default_decoder.decode(s)

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/__init__.py:354: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <json.decoder.JSONDecoder object at 0x101f9b6d8>, s = ''
_w = <built-in method match of _sre.SRE_Pattern object at 0x101e60e70>

    def decode(self, s, _w=WHITESPACE.match):
        """Return the Python representation of ``s`` (a ``str`` instance
            containing a JSON document).
    
            """
>       obj, end = self.raw_decode(s, idx=_w(s, 0).end())

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/decoder.py:339: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <json.decoder.JSONDecoder object at 0x101f9b6d8>, s = '', idx = 0

    def raw_decode(self, s, idx=0):
        """Decode a JSON document from ``s`` (a ``str`` beginning with
            a JSON document) and return a 2-tuple of the Python
            representation and the index in ``s`` where the document ended.
    
            This can be used to decode a JSON document from a string that may
            have extraneous data at the end.
    
            """
        try:
            obj, end = self.scan_once(s, idx)
        except StopIteration as err:
>           raise JSONDecodeError("Expecting value", s, err.value) from None
E           json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/decoder.py:357: JSONDecodeError

```

Ok, so this isn't a `KeyError`, it's a `JSONDecodeError`. That seems like exactly the error I'd expect to see: what's the problem here?

The problem  (potentially) is that a valid response with empty content causes `response.json ()` to throw an exception. Is this buggy behavior? Should this case return in something like `{}` instead? I may be incorrect in my model of how such responses work.

No, the exception is the only safe outcome. 

If we returned `{}`, it becomes difficult to tell the difference between no content and a JSON document that reads `{}`. The same applies to the empty string or to `None`: all are possible results of JSON decoding. 

So I think the exception is correct. 

@joshmgrant specifically, this exception is thrown by `json.loads` which is in the standard library. If you call `response.json()` and the `response.content` is empty or even something that's invalid JSON, what else could we do but provide the exception? We didn't do anything wrong, ostensibly, you were expecting valid JSON but that's not what you received. Why would we hide that from you?

This is exactly the information I was missing! This makes sense now and I'll look for handling exceptions in the case of empty `response.content`. Thanks so much!