diff --git a/django/test/client.py b/django/test/client.py index d14ba43792..07641b7e65 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -192,7 +192,12 @@ def encode_multipart(boundary, data): # file, or a *list* of form values and/or files. Remember that HTTP field # names can be duplicated! for (key, value) in data.items(): - if is_file(value): + if value is None: + raise TypeError( + 'Cannot encode None as POST data. Did you mean to pass an ' + 'empty string or omit the value?' + ) + elif is_file(value): lines.extend(encode_file(boundary, key, value)) elif not isinstance(value, str) and is_iterable(value): for item in value: diff --git a/django/utils/http.py b/django/utils/http.py index db18e57803..de1ea71368 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -91,20 +91,31 @@ def urlencode(query, doseq=False): query = query.items() query_params = [] for key, value in query: - if isinstance(value, (str, bytes)): + if value is None: + raise TypeError( + 'Cannot encode None in a query string. Did you mean to pass ' + 'an empty string or omit the value?' + ) + elif isinstance(value, (str, bytes)): query_val = value else: try: - iter(value) + itr = iter(value) except TypeError: query_val = value else: # Consume generators and iterators, even when doseq=True, to # work around https://bugs.python.org/issue31706. - query_val = [ - item if isinstance(item, bytes) else str(item) - for item in value - ] + query_val = [] + for item in itr: + if item is None: + raise TypeError( + 'Cannot encode None in a query string. Did you ' + 'mean to pass an empty string or omit the value?' + ) + elif not isinstance(item, bytes): + item = str(item) + query_val.append(item) query_params.append((key, query_val)) return original_urlencode(query_params, doseq) diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index f731562640..3201d00047 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -457,6 +457,11 @@ Miscellaneous * Tests that violate deferrable database constraints now error when run on SQLite 3.20+, just like on other backends that support such constraints. +* To catch usage mistakes, the test :class:`~django.test.Client` and + :func:`django.utils.http.urlencode` now raise ``TypeError`` if ``None`` is + passed as a value to encode because ``None`` can't be encoded in GET and POST + data. Either pass an empty string or omit the value. + .. _deprecated-features-2.2: Features deprecated in 2.2 diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index bb6c8bbff0..432865328f 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -59,6 +59,14 @@ class ClientTest(TestCase): response = self.client.get('/get_view/?var=1\ufffd') self.assertEqual(response.context['var'], '1\ufffd') + def test_get_data_none(self): + msg = ( + 'Cannot encode None in a query string. Did you mean to pass an ' + 'empty string or omit the value?' + ) + with self.assertRaisesMessage(TypeError, msg): + self.client.get('/get_view/', {'value': None}) + def test_get_post_view(self): "GET a view that normally expects POSTs" response = self.client.get('/post_view/', {}) @@ -92,6 +100,14 @@ class ClientTest(TestCase): self.assertEqual(response.templates[0].name, 'POST Template') self.assertContains(response, 'Data received') + def test_post_data_none(self): + msg = ( + 'Cannot encode None as POST data. Did you mean to pass an empty ' + 'string or omit the value?' + ) + with self.assertRaisesMessage(TypeError, msg): + self.client.post('/post_view/', {'value': None}) + def test_json_serialization(self): """The test client serializes JSON data.""" methods = ('post', 'put', 'patch', 'delete') diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py index 93f45fb936..aca825ef1f 100644 --- a/tests/utils_tests/test_http.py +++ b/tests/utils_tests/test_http.py @@ -12,7 +12,12 @@ from django.utils.http import ( ) -class URLEncodeTests(unittest.TestCase): +class URLEncodeTests(SimpleTestCase): + cannot_encode_none_msg = ( + 'Cannot encode None in a query string. Did you mean to pass an ' + 'empty string or omit the value?' + ) + def test_tuples(self): self.assertEqual(urlencode((('a', 1), ('b', 2), ('c', 3))), 'a=1&b=2&c=3') @@ -65,6 +70,20 @@ class URLEncodeTests(unittest.TestCase): self.assertEqual(urlencode({'a': gen()}, doseq=True), 'a=0&a=1') self.assertEqual(urlencode({'a': gen()}, doseq=False), 'a=%5B%270%27%2C+%271%27%5D') + def test_none(self): + with self.assertRaisesMessage(TypeError, self.cannot_encode_none_msg): + urlencode({'a': None}) + + def test_none_in_sequence(self): + with self.assertRaisesMessage(TypeError, self.cannot_encode_none_msg): + urlencode({'a': [None]}, doseq=True) + + def test_none_in_generator(self): + def gen(): + yield None + with self.assertRaisesMessage(TypeError, self.cannot_encode_none_msg): + urlencode({'a': gen()}, doseq=True) + class Base36IntTests(SimpleTestCase): def test_roundtrip(self):