Repository: falconry/falcon

Issue:
Add an option to RequestOptions to toggle parsing of form params
Add an option to RequestOptions to enable/disable parsing form params as currently implemented. It would be disabled by default (breaking change, but easy to work around). This would mitigate the problems people have experienced re the request stream being automatically slurped when the POSTed content type is `application/x-www-form-urlencoded`. 

This would not preclude working on a longer-term strategy for handling forms in a manner distinct from query parameters.

See also:
- #418
- #493

Discussion:

Patch:
diff --git a/falcon/request.py b/falcon/request.py
index c7322b4..eac9b95 100644
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -167,19 +167,13 @@ class Request(object):
 
             Note:
                 If an HTML form is POSTed to the API using the
-                *application/x-www-form-urlencoded* media type, Falcon
+                *application/x-www-form-urlencoded* media type, and
+                the :py:attr:`~.RequestOptions.auto_parse_form_urlencoded`
+                option is set, the framework
                 will consume `stream` in order to parse the parameters
                 and merge them into the query string parameters. In this
                 case, the stream will be left at EOF.
 
-                Note also that 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.
-
-                Falcon expects form-encoded request bodies to be
-                encoded according to the standard W3C algorithm (see
-                also http://goo.gl/6rlcux).
-
         date (datetime): Value of the Date header, converted to a
             ``datetime`` instance. The header value is assumed to
             conform to RFC 1123.
@@ -320,7 +314,8 @@ class Request(object):
         # PERF(kgriffs): Technically, we should spend a few more
         # cycles and parse the content type for real, but
         # this heuristic will work virtually all the time.
-        if (self.content_type is not None and
+        if (self.options.auto_parse_form_urlencoded and
+                self.content_type is not None and
                 'application/x-www-form-urlencoded' in self.content_type):
             self._parse_form_urlencoded()
 
@@ -1159,11 +1154,28 @@ class RequestOptions(object):
     Attributes:
         keep_blank_qs_values (bool): Set to ``True`` in order to retain
             blank values in query string parameters (default ``False``).
+        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
+            request's content type is
+            *application/x-www-form-urlencoded* (default ``False``). In
+            this case, the request's content stream will be left at EOF.
+
+            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.
+
+                Falcon expects form-encoded request bodies to be
+                encoded according to the standard W3C algorithm (see
+                also http://goo.gl/6rlcux).
 
     """
     __slots__ = (
         'keep_blank_qs_values',
+        'auto_parse_form_urlencoded',
     )
 
     def __init__(self):
         self.keep_blank_qs_values = False
+        self.auto_parse_form_urlencoded = False


Test patch:
diff --git a/tests/test_query_params.py b/tests/test_query_params.py
index 50ed010..93a429f 100644
--- a/tests/test_query_params.py
+++ b/tests/test_query_params.py
@@ -473,6 +473,10 @@ class _TestQueryParams(testing.TestBase):
 
 
 class PostQueryParams(_TestQueryParams):
+    def before(self):
+        super(PostQueryParams, self).before()
+        self.api.req_options.auto_parse_form_urlencoded = True
+
     def simulate_request(self, path, query_string, **kwargs):
         headers = {"Content-Type": "application/x-www-form-urlencoded"}
         super(PostQueryParams, self).simulate_request(
@@ -484,10 +488,29 @@ class PostQueryParams(_TestQueryParams):
         self.simulate_request('/', query_string=query_string)
 
         req = self.resource.req
-        self.assertEqual(req.get_param('q'), None)
+        self.assertIs(req.get_param('q'), None)
+
+    def test_explicitly_disable_auto_parse(self):
+        self.api.req_options.auto_parse_form_urlencoded = False
+        self.simulate_request('/', query_string='q=42')
+
+        req = self.resource.req
+        self.assertIs(req.get_param('q'), None)
 
 
 class GetQueryParams(_TestQueryParams):
     def simulate_request(self, path, query_string, **kwargs):
         super(GetQueryParams, self).simulate_request(
             path, query_string=query_string, **kwargs)
+
+
+class PostQueryParamsDefaultBehavior(testing.TestBase):
+    def test_dont_auto_parse_by_default(self):
+        self.resource = testing.TestResource()
+        self.api.add_route('/', self.resource)
+
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        self.simulate_request('/', body='q=42', headers=headers)
+
+        req = self.resource.req
+        self.assertIs(req.get_param('q'), None)