Repository: kevin1024/vcrpy

Issue:
AttributeError: 'NoneType' object has no attribute 'decode' when 'body' in match_on, fixed with 'raw_body'
When using vcr with the following configuration with 'body', there are errors with loading the JSON response -- it could be due to a blank response or blank payload. When I use 'raw_body' it works perfectly. Here's the stack trace (relevant sections):

```bash
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/requests/sessions.py", line 473, in get
    return self.request('GET', url, **kwargs)
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/requests/sessions.py", line 461, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/requests/sessions.py", line 573, in send
    r = adapter.send(request, **kwargs)
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/requests/adapters.py", line 370, in send
    timeout=timeout
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/requests/packages/urllib3/connectionpool.py", line 518, in urlopen
    body=body, headers=headers)
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/requests/packages/urllib3/connectionpool.py", line 353, in _make_request
    httplib_response = conn.getresponse(buffering=True)
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/vcr/stubs/__init__.py", line 219, in getresponse
    if self.cassette.can_play_response_for(self._vcr_request):
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/vcr/cassette.py", line 232, in can_play_response_for
    return request and request in self and \
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/vcr/cassette.py", line 303, in __contains__
    for index, response in self._responses(request):
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/vcr/cassette.py", line 227, in _responses
    if requests_match(request, stored_request, self._match_on):
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/vcr/matchers.py", line 97, in requests_match
    matches = [(m(r1, r2), m) for m in matchers]
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/vcr/matchers.py", line 80, in body
    return transformer(read_body(r1)) == transformer(read_body(r2))
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/vcr/matchers.py", line 52, in _transform_json
    return json.loads(body.decode('utf-8'))
AttributeError: 'NoneType' object has no attribute 'decode'
```

Comments:

Patch:
diff --git a/vcr/matchers.py b/vcr/matchers.py
index b54ed2f..8fe334e 100644
--- a/vcr/matchers.py
+++ b/vcr/matchers.py
@@ -49,7 +49,8 @@ def _transform_json(body):
     # Request body is always a byte string, but json.loads() wants a text
     # string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
     # and UTF-32 are also allowed: hmmmmm).
-    return json.loads(body.decode('utf-8'))
+    if body:
+        return json.loads(body.decode('utf-8'))
 
 
 _xml_header_checker = _header_checker('text/xml')
diff --git a/vcr/serializers/compat.py b/vcr/serializers/compat.py
index 0fcc583..8364997 100644
--- a/vcr/serializers/compat.py
+++ b/vcr/serializers/compat.py
@@ -24,7 +24,7 @@ def convert_body_to_bytes(resp):
     http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
     """
     try:
-        if not isinstance(resp['body']['string'], six.binary_type):
+        if resp['body']['string'] is not None and not isinstance(resp['body']['string'], six.binary_type):
             resp['body']['string'] = resp['body']['string'].encode('utf-8')
     except (KeyError, TypeError, UnicodeEncodeError):
         # The thing we were converting either wasn't a dictionary or didn't
diff --git a/vcr/stubs/aiohttp_stubs/__init__.py b/vcr/stubs/aiohttp_stubs/__init__.py
index a19be69..5730d3b 100644
--- a/vcr/stubs/aiohttp_stubs/__init__.py
+++ b/vcr/stubs/aiohttp_stubs/__init__.py
@@ -4,8 +4,9 @@ from __future__ import absolute_import
 import asyncio
 import functools
 import json
+import urllib
 
-from aiohttp import ClientResponse
+from aiohttp import ClientResponse, helpers
 
 from vcr.request import Request
 
@@ -26,15 +27,38 @@ class MockClientResponse(ClientResponse):
 
 
 def vcr_request(cassette, real_request):
-
     @functools.wraps(real_request)
     @asyncio.coroutine
     def new_request(self, method, url, **kwargs):
         headers = kwargs.get('headers')
         headers = self._prepare_headers(headers)
         data = kwargs.get('data')
-
-        vcr_request = Request(method, url, data, headers)
+        params = kwargs.get('params')
+
+        # INFO: Query join logic from
+        # https://github.com/KeepSafe/aiohttp/blob/b3eeedbc2f515ec2aa6e87ba129524c17b6fe4e3/aiohttp/client_reqrep.py#L167-L188
+        scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
+        if not path:
+            path = '/'
+
+        # NOTICE: Not sure this is applicable here:
+        # if isinstance(params, collections.Mapping):
+        #     params = list(params.items())
+
+        if params:
+            if not isinstance(params, str):
+                params = urllib.parse.urlencode(params)
+            if query:
+                query = '%s&%s' % (query, params)
+            else:
+                query = params
+
+        request_path = urllib.parse.urlunsplit(('', '', helpers.requote_uri(path),
+                                                query, fragment))
+        request_url = urllib.parse.urlunsplit(
+            (scheme, netloc, request_path, '', ''))
+
+        vcr_request = Request(method, request_url, data, headers)
 
         if cassette.can_play_response_for(vcr_request):
             vcr_response = cassette.play_response(vcr_request)


Test patch:
diff --git a/tests/integration/test_aiohttp.py b/tests/integration/test_aiohttp.py
index 1aa9c05..82957ff 100644
--- a/tests/integration/test_aiohttp.py
+++ b/tests/integration/test_aiohttp.py
@@ -85,3 +85,33 @@ def test_post(tmpdir, scheme):
         _, cassette_response_json = post(url, data=data)
         assert cassette_response_json == response_json
         assert cassette.play_count == 1
+
+
+def test_params(tmpdir, scheme):
+    url = scheme + '://httpbin.org/get'
+    params = {'a': 1, 'b': False, 'c': 'c'}
+    with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
+        _, response_json = get(url, as_text=False, params=params)
+
+    with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
+        _, cassette_response_json = get(url, as_text=False, params=params)
+        assert cassette_response_json == response_json
+        assert cassette.play_count == 1
+
+
+def test_params_same_url_distinct_params(tmpdir, scheme):
+    url = scheme + '://httpbin.org/get'
+    params = {'a': 1, 'b': False, 'c': 'c'}
+    with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
+        _, response_json = get(url, as_text=False, params=params)
+
+    with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
+        _, cassette_response_json = get(url, as_text=False, params=params)
+        assert cassette_response_json == response_json
+        assert cassette.play_count == 1
+
+    other_params = {'other': 'params'}
+    with vcr.use_cassette(str(tmpdir.join('get.yaml'))) as cassette:
+        response, cassette_response_text = get(url, as_text=True, params=other_params)
+        assert 'No match for the request' in cassette_response_text
+        assert response.status == 599
diff --git a/tests/integration/test_requests.py b/tests/integration/test_requests.py
index 84a992b..804ab2e 100644
--- a/tests/integration/test_requests.py
+++ b/tests/integration/test_requests.py
@@ -38,6 +38,18 @@ def test_body(tmpdir, httpbin_both):
         assert content == requests.get(url).content
 
 
+def test_get_empty_content_type_json(tmpdir, httpbin_both):
+    '''Ensure GET with application/json content-type and empty request body doesn't crash'''
+    url = httpbin_both + '/status/200'
+    headers = {'Content-Type': 'application/json'}
+
+    with vcr.use_cassette(str(tmpdir.join('get_empty_json.yaml')), match_on=('body',)):
+        status = requests.get(url, headers=headers).status_code
+
+    with vcr.use_cassette(str(tmpdir.join('get_empty_json.yaml')), match_on=('body',)):
+        assert status == requests.get(url, headers=headers).status_code
+
+
 def test_effective_url(tmpdir, httpbin_both):
     '''Ensure that the effective_url is captured'''
     url = httpbin_both.url + '/redirect-to?url=/html'
diff --git a/tests/unit/test_serialize.py b/tests/unit/test_serialize.py
index cf3f0a8..6555cca 100644
--- a/tests/unit/test_serialize.py
+++ b/tests/unit/test_serialize.py
@@ -4,7 +4,7 @@ import pytest
 from vcr.compat import mock
 from vcr.request import Request
 from vcr.serialize import deserialize, serialize
-from vcr.serializers import yamlserializer, jsonserializer
+from vcr.serializers import yamlserializer, jsonserializer, compat
 
 
 def test_deserialize_old_yaml_cassette():
@@ -131,3 +131,9 @@ def test_serialize_binary_request():
         )
     except (UnicodeDecodeError, TypeError) as exc:
         assert msg in str(exc)
+
+
+def test_deserialize_no_body_string():
+    data = {'body': {'string': None}}
+    output = compat.convert_to_bytes(data)
+    assert data == output