Repository: falconry/falcon

Issue:
Revisit RequestOptions.auto_parse_form_urlencoded
Create a media handler for `application/x-www-form-urlencoded` that can be used via `req.media`. As part of this work, deprecate the use of the `auto_parse_form_urlencoded` (which merges form params into query string params) by adding a note to the docs to that effect.

The implementation can be patterned after `Request._parse_form_urlencoded()`


Discussion:
Work remaining after https://github.com/falconry/falcon/pull/1537:
* Decide whether we want to add this to default handlers (@nZac expressed support for this in another discussion IIRC, I agree)
* Deprecate `auto_parse_form_urlencoded` in the docs

Resolving patch:
diff --git a/falcon/media/handlers.py b/falcon/media/handlers.py
--- a/falcon/media/handlers.py
+++ b/falcon/media/handlers.py
@@ -1,7 +1,9 @@
 from collections import UserDict
 
 from falcon import errors
+from falcon.constants import MEDIA_URLENCODED
 from falcon.media import JSONHandler
+from falcon.media import URLEncodedFormHandler
 from falcon.vendor import mimeparse
 
 
@@ -11,6 +13,7 @@ def __init__(self, initial=None):
         handlers = initial or {
             'application/json': JSONHandler(),
             'application/json; charset=UTF-8': JSONHandler(),
+            MEDIA_URLENCODED: URLEncodedFormHandler(),
         }
 
         # NOTE(jmvrbanac): Directly calling UserDict as it's not inheritable.
diff --git a/falcon/request.py b/falcon/request.py
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -1164,6 +1164,14 @@ def get_param(self, name, required=False, store=None, default=None):
             :py:attr:`~.RequestOptions.auto_parse_form_urlencoded` to
             ``True`` via :any:`App.req_options`.
 
+            Note, however, that the
+            :attr:`~.RequestOptions.auto_parse_form_urlencoded` option is
+            considered deprecated as of Falcon 3.0 in favor of accessing the
+            URL-encoded form via :attr:`~Request.media`, and it may be removed
+            in a future release.
+
+            See also: :ref:`access_urlencoded_form`
+
         Note:
             Similar to the way multiple keys in form data is handled,
             if a query parameter is assigned a comma-separated list of
@@ -1812,6 +1820,14 @@ class RequestOptions:
             Enabling this option makes the form parameters accessible
             via :attr:`~.params`, :meth:`~.get_param`, etc.
 
+            Warning:
+                The `auto_parse_form_urlencoded` option is considered
+                deprecated as of Falcon 3.0 in favor of accessing the
+                URL-encoded form via :attr:`~Request.media`, and it may be
+                removed in a future release.
+
+                See also: :ref:`access_urlencoded_form`
+
             Warning:
                 When this option is enabled, the request's body
                 stream will be left at EOF. The original data is


Test patch:
diff --git a/tests/test_media_urlencoded.py b/tests/test_media_urlencoded.py
--- a/tests/test_media_urlencoded.py
+++ b/tests/test_media_urlencoded.py
@@ -37,17 +37,9 @@ def on_post(self, req, resp):
 
 @pytest.fixture
 def client():
-    handlers = media.Handlers({
-        falcon.MEDIA_JSON: media.JSONHandler(),
-        falcon.MEDIA_URLENCODED: media.URLEncodedFormHandler(),
-    })
-    api = falcon.API()
-    api.req_options.media_handlers = handlers
-    api.resp_options.media_handlers = handlers
-
-    api.add_route('/media', MediaMirror())
-
-    return testing.TestClient(api)
+    app = falcon.App()
+    app.add_route('/media', MediaMirror())
+    return testing.TestClient(app)
 
 
 def test_empty_form(client):
@@ -61,6 +53,11 @@ def test_empty_form(client):
     ('a=1&b=&c=3', {'a': '1', 'b': '', 'c': '3'}),
     ('param=undefined', {'param': 'undefined'}),
     ('color=green&color=black', {'color': ['green', 'black']}),
+    (
+        'food=hamburger+%28%F0%9F%8D%94%29&sauce=BBQ',
+        {'food': 'hamburger (🍔)', 'sauce': 'BBQ'},
+    ),
+    ('flag%1&flag%2&flag%1&flag%2', {'flag%1': ['', ''], 'flag%2': ['', '']}),
 ])
 def test_urlencoded_form(client, body, expected):
     resp = client.simulate_post(