feat(util,Request): Add options to disable csv parsing of query params (#820)
* Add option to to_query_str to encode lists as multiple occurences of the same parameter. * Add option to parse_query_string to not split comma-delimited values. * Add an option to RequestOptions for toggling csv parsing. In all cases, the options default to the previous behavior in order to avoid introducing any breaking changes. Co-Authored-By: Daniel Schwarz <qwesda@me.com> Co-Authored-By: Steven Ly <sly@openroad.ca> Co-Authored-By: Kurt Griffiths <inbox@kgriffs.com> Closes #749
This commit is contained in:
@@ -284,6 +284,7 @@ class Request(object):
|
||||
self._params = parse_query_string(
|
||||
self.query_string,
|
||||
keep_blank_qs_values=self.options.keep_blank_qs_values,
|
||||
parse_qs_csv=self.options.auto_parse_qs_csv,
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -1153,6 +1154,7 @@ class Request(object):
|
||||
extra_params = parse_query_string(
|
||||
body,
|
||||
keep_blank_qs_values=self.options.keep_blank_qs_values,
|
||||
parse_qs_csv=self.options.auto_parse_qs_csv,
|
||||
)
|
||||
|
||||
self._params.update(extra_params)
|
||||
@@ -1190,8 +1192,11 @@ class RequestOptions(object):
|
||||
"""This class is a container for ``Request`` options.
|
||||
|
||||
Attributes:
|
||||
keep_blank_qs_values (bool): Set to ``True`` in order to retain
|
||||
blank values in query string parameters (default ``False``).
|
||||
keep_blank_qs_values (bool): Set to ``True`` to keep query string
|
||||
fields even if 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.
|
||||
auto_parse_form_urlencoded: Set to ``True`` in order to
|
||||
automatically consume the request stream and merge the
|
||||
results into the request's query string params when the
|
||||
@@ -1202,18 +1207,29 @@ class RequestOptions(object):
|
||||
Note:
|
||||
The character encoding for fields, before
|
||||
percent-encoding non-ASCII bytes, is assumed to be
|
||||
UTF-8. The special `_charset_` field is ignored if present.
|
||||
UTF-8. The special `_charset_` field is ignored if
|
||||
present.
|
||||
|
||||
Falcon expects form-encoded request bodies to be
|
||||
encoded according to the standard W3C algorithm (see
|
||||
also http://goo.gl/6rlcux).
|
||||
|
||||
auto_parse_qs_csv: Set to ``False`` to treat commas in a query
|
||||
string value as literal characters, rather than as a comma-
|
||||
separated list (default ``True``). When this option is
|
||||
enabled, the value will be split on any non-percent-encoded
|
||||
commas. Disable this option when encoding lists as multiple
|
||||
occurrences of the same parameter, and when values may be
|
||||
encoded in alternative formats in which the comma character
|
||||
is significant.
|
||||
"""
|
||||
__slots__ = (
|
||||
'keep_blank_qs_values',
|
||||
'auto_parse_form_urlencoded',
|
||||
'auto_parse_qs_csv',
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.keep_blank_qs_values = False
|
||||
self.auto_parse_form_urlencoded = False
|
||||
self.auto_parse_qs_csv = True
|
||||
|
||||
@@ -148,7 +148,7 @@ def http_date_to_dt(http_date, obs_date=False):
|
||||
raise ValueError('time data %r does not match known formats' % http_date)
|
||||
|
||||
|
||||
def to_query_str(params):
|
||||
def to_query_str(params, comma_delimited_lists=True):
|
||||
"""Converts a dictionary of params to a query string.
|
||||
|
||||
Args:
|
||||
@@ -157,6 +157,10 @@ def to_query_str(params):
|
||||
something that can be converted into a ``str``. If `params`
|
||||
is a ``list``, it will be converted to a comma-delimited string
|
||||
of values (e.g., 'thing=1,2,3')
|
||||
comma_delimited_lists (bool, default ``True``):
|
||||
If set to ``False`` encode lists by specifying multiple instances
|
||||
of the parameter (e.g., 'thing=1&thing=2&thing=3')
|
||||
|
||||
|
||||
Returns:
|
||||
str: A URI query string including the '?' prefix, or an empty string
|
||||
@@ -175,7 +179,20 @@ def to_query_str(params):
|
||||
elif v is False:
|
||||
v = 'false'
|
||||
elif isinstance(v, list):
|
||||
v = ','.join(map(str, v))
|
||||
if comma_delimited_lists:
|
||||
v = ','.join(map(str, v))
|
||||
else:
|
||||
for list_value in v:
|
||||
if list_value is True:
|
||||
list_value = 'true'
|
||||
elif list_value is False:
|
||||
list_value = 'false'
|
||||
else:
|
||||
list_value = str(list_value)
|
||||
|
||||
query_str += k + '=' + list_value + '&'
|
||||
|
||||
continue
|
||||
else:
|
||||
v = str(v)
|
||||
|
||||
|
||||
@@ -246,11 +246,12 @@ else:
|
||||
return decoded_uri.decode('utf-8', 'replace')
|
||||
|
||||
|
||||
def parse_query_string(query_string, keep_blank_qs_values=False):
|
||||
def parse_query_string(query_string, keep_blank_qs_values=False,
|
||||
parse_qs_csv=True):
|
||||
"""Parse a query string into a dict.
|
||||
|
||||
Query string parameters are assumed to use standard form-encoding. Only
|
||||
parameters with values are parsed. for example, given 'foo=bar&flag',
|
||||
parameters with values are returned. For example, given 'foo=bar&flag',
|
||||
this function would ignore 'flag' unless the `keep_blank_qs_values` option
|
||||
is set.
|
||||
|
||||
@@ -269,8 +270,16 @@ def parse_query_string(query_string, keep_blank_qs_values=False):
|
||||
|
||||
Args:
|
||||
query_string (str): The query string to parse.
|
||||
keep_blank_qs_values (bool): If set to ``True``, preserves boolean
|
||||
fields and fields with no content as blank strings.
|
||||
keep_blank_qs_values (bool): Set to ``True`` to return fields even if
|
||||
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.
|
||||
parse_qs_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.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of (*name*, *value*) pairs, one per query
|
||||
@@ -309,7 +318,7 @@ def parse_query_string(query_string, keep_blank_qs_values=False):
|
||||
params[k] = [old_value, decode(v)]
|
||||
|
||||
else:
|
||||
if ',' in v:
|
||||
if parse_qs_csv and ',' in v:
|
||||
# NOTE(kgriffs): Falcon supports a more compact form of
|
||||
# lists, in which the elements are comma-separated and
|
||||
# assigned to a single param instance. If it turns out that
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import ddt
|
||||
|
||||
from falcon.request import RequestOptions
|
||||
import falcon.testing as testing
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestRequestOptions(testing.TestBase):
|
||||
|
||||
def test_correct_options(self):
|
||||
def test_option_defaults(self):
|
||||
options = RequestOptions()
|
||||
|
||||
self.assertFalse(options.keep_blank_qs_values)
|
||||
options.keep_blank_qs_values = True
|
||||
self.assertTrue(options.keep_blank_qs_values)
|
||||
options.keep_blank_qs_values = False
|
||||
self.assertFalse(options.keep_blank_qs_values)
|
||||
self.assertFalse(options.auto_parse_form_urlencoded)
|
||||
self.assertTrue(options.auto_parse_qs_csv)
|
||||
|
||||
@ddt.data(
|
||||
'keep_blank_qs_values',
|
||||
'auto_parse_form_urlencoded',
|
||||
'auto_parse_qs_csv',
|
||||
)
|
||||
def test_options_toggle(self, option_name):
|
||||
options = RequestOptions()
|
||||
|
||||
setattr(options, option_name, True)
|
||||
self.assertTrue(getattr(options, option_name))
|
||||
|
||||
setattr(options, option_name, False)
|
||||
self.assertFalse(getattr(options, option_name))
|
||||
|
||||
def test_incorrect_options(self):
|
||||
options = RequestOptions()
|
||||
|
||||
@@ -65,6 +65,60 @@ class _TestQueryParams(testing.TestBase):
|
||||
self.assertEqual(req.get_param_as_list('id', int), [23, 42])
|
||||
self.assertEqual(req.get_param('q'), u'\u8c46 \u74e3')
|
||||
|
||||
def test_option_auto_parse_qs_csv_simple_false(self):
|
||||
self.api.req_options.auto_parse_qs_csv = False
|
||||
|
||||
query_string = 'id=23,42,,&id=2'
|
||||
self.simulate_request('/', query_string=query_string)
|
||||
|
||||
req = self.resource.req
|
||||
|
||||
self.assertEqual(req.params['id'], [u'23,42,,', u'2'])
|
||||
self.assertIn(req.get_param('id'), [u'23,42,,', u'2'])
|
||||
self.assertEqual(req.get_param_as_list('id'), [u'23,42,,', u'2'])
|
||||
|
||||
def test_option_auto_parse_qs_csv_simple_true(self):
|
||||
self.api.req_options.auto_parse_qs_csv = True
|
||||
|
||||
query_string = 'id=23,42,,&id=2'
|
||||
self.simulate_request('/', query_string=query_string)
|
||||
|
||||
req = self.resource.req
|
||||
|
||||
self.assertEqual(req.params['id'], [u'23', u'42', u'2'])
|
||||
self.assertIn(req.get_param('id'), [u'23', u'42', u'2'])
|
||||
self.assertEqual(req.get_param_as_list('id', int), [23, 42, 2])
|
||||
|
||||
def test_option_auto_parse_qs_csv_complex_false(self):
|
||||
self.api.req_options.auto_parse_qs_csv = False
|
||||
|
||||
encoded_json = '%7B%22msg%22:%22Testing%201,2,3...%22,%22code%22:857%7D'
|
||||
decoded_json = '{"msg":"Testing 1,2,3...","code":857}'
|
||||
|
||||
query_string = ('colors=red,green,blue&limit=1'
|
||||
'&list-ish1=f,,x&list-ish2=,0&list-ish3=a,,,b'
|
||||
'&empty1=&empty2=,&empty3=,,'
|
||||
'&thing=' + encoded_json)
|
||||
|
||||
self.simulate_request('/', query_string=query_string)
|
||||
|
||||
req = self.resource.req
|
||||
|
||||
self.assertIn(req.get_param('colors'), 'red,green,blue')
|
||||
self.assertEqual(req.get_param_as_list('colors'), [u'red,green,blue'])
|
||||
|
||||
self.assertEqual(req.get_param_as_list('limit'), ['1'])
|
||||
|
||||
self.assertEqual(req.get_param_as_list('empty1'), None)
|
||||
self.assertEqual(req.get_param_as_list('empty2'), [u','])
|
||||
self.assertEqual(req.get_param_as_list('empty3'), [u',,'])
|
||||
|
||||
self.assertEqual(req.get_param_as_list('list-ish1'), [u'f,,x'])
|
||||
self.assertEqual(req.get_param_as_list('list-ish2'), [u',0'])
|
||||
self.assertEqual(req.get_param_as_list('list-ish3'), [u'a,,,b'])
|
||||
|
||||
self.assertEqual(req.get_param('thing'), decoded_json)
|
||||
|
||||
def test_bad_percentage(self):
|
||||
query_string = 'x=%%20%+%&y=peregrine&z=%a%z%zz%1%20e'
|
||||
self.simulate_request('/', query_string=query_string)
|
||||
|
||||
@@ -128,6 +128,16 @@ class TestFalconUtils(testtools.TestCase):
|
||||
falcon.to_query_str({'things': ['a', 'b']}),
|
||||
'?things=a,b')
|
||||
|
||||
expected = ('?things=a&things=b&things=&things=None'
|
||||
'&things=true&things=false&things=0')
|
||||
|
||||
actual = falcon.to_query_str(
|
||||
{'things': ['a', 'b', '', None, True, False, 0]},
|
||||
comma_delimited_lists=False
|
||||
)
|
||||
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_pack_query_params_several(self):
|
||||
garbage_in = {
|
||||
'limit': 17,
|
||||
|
||||
Reference in New Issue
Block a user