Repository: falconry/falcon

Issue:
Empty value in query string should not be parsed into None
Now, the falcon query string can not distinguish `field1=&field2=value2` from `field2=value2`. The `field1` will be None anyway.



Discussion:
Verified. With the latest code, `foo=1&foo=&foo=44` will be parsed into `[('foo', '1'), ('foo', '44')]` which will subsequently be folded into: `params['foo'] = [1, 44]`. However, the equivalent comma-delimited "API" form `foo=1,,44' will result in`[1, None, 44]`.

It should be easy to fix this by simply modifying the list comprehensions in `Request.get_param_as_list`.

This is a breaking change, but I would be surprised if anyone were relying on it, and even then I think it is important to do it rather than leave this inconsistency in the code as technical debt that may come back to bite us down the road.

On Oct 01, 2014, at 03:00 PM, Kurt Griffiths wrote:

> This is a breaking change, but I would be surprised if anyone were relying on
> it, and even then I think it is important to do it rather than leave this
> inconsistency in the code as technical debt that may come back to bite us
> down the road.

+1, especially given that full support for &-separated multiple key values was
really only added in an as-yet-unreleased version.

kk, I'll take care of it.


Resolving patch:
diff --git a/falcon/request.py b/falcon/request.py
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -692,22 +692,18 @@ def get_param_as_list(self, name,
 
         Returns:
             list: The value of the param if it is found. Otherwise, returns
-            *None* unless required is True. for partial lists, *None* will be
-            returned as a placeholder. For example::
+            *None* unless required is True. Empty list elements will be
+            discarded. For example a query string containing this::
 
                 things=1,,3
 
-            would be returned as::
+            or a query string containing this::
 
-                ['1', None, '3']
+                things=1&things=&things=3
 
-            while this::
+            would both result in::
 
-                things=,,,
-
-            would just be retured as::
-
-                [None, None, None, None]
+                ['1', '3']
 
         Raises
             HTTPBadRequest: The param was not found in the request, but was
@@ -730,13 +726,10 @@ def get_param_as_list(self, name,
             # PERF(kgriffs): Use if-else rather than a DRY approach
             # that sets transform to a passthrough function; avoids
             # function calling overhead.
-            if transform is None:
-                items = [i if i != '' else None
-                         for i in items]
-            else:
+            if transform is not None:
                 try:
-                    items = [transform(i) if i != '' else None
-                             for i in items]
+                    items = [transform(i) for i in items]
+
                 except ValueError:
                     msg = 'The value is not formatted correctly.'
                     raise InvalidParam(msg, name)
diff --git a/falcon/util/uri.py b/falcon/util/uri.py
--- a/falcon/util/uri.py
+++ b/falcon/util/uri.py
@@ -273,8 +273,8 @@ def parse_query_string(query_string):
 
     Returns:
         dict: A dict containing ``(name, value)`` tuples, one per query
-            parameter. Note that *value* will be a string, and that *name* is
-            case-sensitive, both copied directly from the query string.
+            parameter. Note that *value* will be a string or list of
+            strings.
 
     Raises:
         TypeError: query_string was not a string or buffer
@@ -303,6 +303,11 @@ def parse_query_string(query_string):
                 # point.
                 v = v.split(',')
 
+                # NOTE(kgriffs): Normalize the result in the case that
+                # some elements are empty strings, such that the result
+                # will be the same for 'foo=1,,3' as 'foo=1&foo=&foo=3'.
+                v = [element for element in v if element]
+
             params[k] = v
 
     return params


Test patch:
diff --git a/tests/test_query_params.py b/tests/test_query_params.py
--- a/tests/test_query_params.py
+++ b/tests/test_query_params.py
@@ -199,7 +199,9 @@ def test_boolean(self):
     def test_list_type(self):
         query_string = ('colors=red,green,blue&limit=1'
                         '&list-ish1=f,,x&list-ish2=,0&list-ish3=a,,,b'
-                        '&empty1=&empty2=,&empty3=,,')
+                        '&empty1=&empty2=,&empty3=,,'
+                        '&thing_one=1,,3'
+                        '&thing_two=1&thing_two=&thing_two=3')
         self.simulate_request('/', query_string=query_string)
 
         req = self.resource.req
@@ -214,19 +216,25 @@ def test_list_type(self):
         self.assertIs(req.get_param_as_list('marker'), None)
 
         self.assertEqual(req.get_param_as_list('empty1'), None)
-        self.assertEqual(req.get_param_as_list('empty2'), [None, None])
-        self.assertEqual(req.get_param_as_list('empty3'), [None, None, None])
+        self.assertEqual(req.get_param_as_list('empty2'), [])
+        self.assertEqual(req.get_param_as_list('empty3'), [])
 
         self.assertEqual(req.get_param_as_list('list-ish1'),
-                         ['f', None, 'x'])
+                         ['f', 'x'])
 
         # Ensure that '0' doesn't get translated to None
         self.assertEqual(req.get_param_as_list('list-ish2'),
-                         [None, '0'])
+                         ['0'])
 
         # Ensure that '0' doesn't get translated to None
         self.assertEqual(req.get_param_as_list('list-ish3'),
-                         ['a', None, None, 'b'])
+                         ['a', 'b'])
+
+        # Ensure consistency between list conventions
+        self.assertEqual(req.get_param_as_list('thing_one'),
+                         ['1', '3'])
+        self.assertEqual(req.get_param_as_list('thing_one'),
+                         req.get_param_as_list('thing_two'))
 
         store = {}
         self.assertEqual(req.get_param_as_list('limit', store=store), ['1'])
@@ -246,11 +254,11 @@ def test_list_transformer(self):
         actual = req.get_param_as_list('coord', transform=float)
         self.assertEqual(actual, expected)
 
-        expected = ['4', None, '1']
+        expected = ['4', '1']
         actual = req.get_param_as_list('things', transform=str)
         self.assertEqual(actual, expected)
 
-        expected = [4, None, 1]
+        expected = [4, 1]
         actual = req.get_param_as_list('things', transform=int)
         self.assertEqual(actual, expected)