Repository: falconry/falcon

Issue:
`parse_query_string()`: change default value of `csv` to False
Some url encoders don't encode comma to %2C

And in such cases, the Falcon Query parser creates an array instead of a string. 

If you have a query string "ABC,ABC" instead of "ABC%2CABC" then if you try to fetch if using 

`msg = falcon.uri.parse_query_string(req.query_string).get("message")`

then msg would be an array instead of string

`msg = ['ABC', 'ABC'] `

which is incorrect


Discussion:
Hi @tahseenjamal!
The behaviour you observe is by design, see the _csv_ parameter in [`parse_query_string()`](https://falcon.readthedocs.io/en/stable/api/util.html#falcon.uri.parse_query_string) and the related explanatory notes.

However, we have changed the default behaviour of [`req.params`](https://falcon.readthedocs.io/en/stable/api/request_and_response_wsgi.html#falcon.Request.params) not to split on comma, see also [`RequestOptions.auto_parse_qs_csv`](https://falcon.readthedocs.io/en/stable/api/app.html#falcon.RequestOptions.auto_parse_qs_csv). As said, given that the default value is now not to automatically split, you could get rid of this side effect by simply changing to
```python
msg = req.params.get("message")
```
Note that you still may get a list back in the case someone sends repeated params, e.g.,`?message=one&message=another`.
You can either add validation for this case, or just grab the first one via
```python
msg = req.get_param("message")
```

Other than that, I agree that this discrepancy is a bit confusing. We should probably change the behaviour of `parse_query_string()` to match the new default of not splitting on comma.

Resolving patch:
diff --git a/falcon/util/uri.py b/falcon/util/uri.py
--- a/falcon/util/uri.py
+++ b/falcon/util/uri.py
@@ -337,13 +337,13 @@ def decode(encoded_uri: str, unquote_plus: bool = True) -> str:
 
 
 def parse_query_string(
-    query_string: str, keep_blank: bool = False, csv: bool = True
+    query_string: str, keep_blank: bool = False, csv: bool = False
 ) -> Dict[str, Union[str, List[str]]]:
     """Parse a query string into a dict.
 
     Query string parameters are assumed to use standard form-encoding. Only
     parameters with values are returned. For example, given 'foo=bar&flag',
-    this function would ignore 'flag' unless the `keep_blank_qs_values` option
+    this function would ignore 'flag' unless the `keep_blank` option
     is set.
 
     Note:
@@ -351,6 +351,8 @@ def parse_query_string(
         lists by repeating a given param multiple times, Falcon supports
         a more compact form in which the param may be given a single time
         but set to a ``list`` of comma-separated elements (e.g., 'foo=a,b,c').
+        This comma-separated format can be enabled by setting the `csv`
+        option (see below) to ``True``.
 
         When using this format, all commas uri-encoded will not be treated by
         Falcon as a delimiter. If the client wants to send a value as a list,
@@ -365,12 +367,13 @@ def parse_query_string(
             they do not have a value (default ``False``). For comma-separated
             values, this option also determines whether or not empty elements
             in the parsed list are retained.
-        csv: Set to ``False`` in order to disable splitting query
-            parameters on ``,`` (default ``True``). Depending on the user agent,
-            encoding lists as multiple occurrences of the same parameter might
-            be preferable. In this case, setting `parse_qs_csv` to ``False``
-            will cause the framework to treat commas as literal characters in
-            each occurring parameter value.
+        csv: Set to ``True`` in order to enable splitting query
+            parameters on ``,`` (default ``False``).
+            Depending on the user agent, encoding lists as multiple occurrences
+            of the same parameter might be preferable. In this case, keeping
+            `parse_qs_csv` at its default value (``False``) will cause the
+            framework to treat commas as literal characters in each occurring
+            parameter value.
 
     Returns:
         dict: A dictionary of (*name*, *value*) pairs, one per query


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -380,13 +380,13 @@ def test_parse_query_string(self):
         result = uri.parse_query_string(query_string)
         assert result['a'] == decoded_url
         assert result['b'] == decoded_json
-        assert result['c'] == ['1', '2', '3']
+        assert result['c'] == '1,2,3'
         assert result['d'] == 'test'
-        assert result['e'] == ['a', '&=,']
+        assert result['e'] == 'a,,&=,'
         assert result['f'] == ['a', 'a=b']
         assert result['é'] == 'a=b'
 
-        result = uri.parse_query_string(query_string, True)
+        result = uri.parse_query_string(query_string, True, True)
         assert result['a'] == decoded_url
         assert result['b'] == decoded_json
         assert result['c'] == ['1', '2', '3']
@@ -395,6 +395,15 @@ def test_parse_query_string(self):
         assert result['f'] == ['a', 'a=b']
         assert result['é'] == 'a=b'
 
+        result = uri.parse_query_string(query_string, csv=True)
+        assert result['a'] == decoded_url
+        assert result['b'] == decoded_json
+        assert result['c'] == ['1', '2', '3']
+        assert result['d'] == 'test'
+        assert result['e'] == ['a', '&=,']
+        assert result['f'] == ['a', 'a=b']
+        assert result['é'] == 'a=b'
+
     @pytest.mark.parametrize(
         'query_string,keep_blank,expected',
         [