Repository: pallets/werkzeug (python)

Issue:
Unexpected 413 - RequestEntityTooLarge response
# Bug
When sending non utf-8 bytes into a form the flask server responds with `413 - RequestEntityTooLarge`. This does not seem like the appropriate http error to return in this case. This happens when using a flask app however it might be a bug in werkzeug.


# Reproduce unexpected behaviour:
app.py:
```py
from flask import *

app = Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def home():
    if request.method == "POST":
        print(request.form["name"])
        print(request.form["email"])

    return render_template("home.html")

if __name__ == "__main__":
    app.run()
```

home.html:
```html
<h1>
    This is home.html
</h1>

<form method="post">

    Name: <input name="name"/>

    <br/><br/>

    Email: <input name="email"/>

    <br/><br/>

    <button type="submit">Submit</button>
</form>
```

bug.py:
```py
import requests


if __name__ == '__main__':
    response1 = requests.post(
        'http://localhost:5000',
        b'\x80',
        headers={'Content-Type':'application/x-www-form-urlencoded', 'Content-Length':str(len(b'\x80'))}
    )
    print("response with only \x80 in content: " + str(response1.status_code) + "\n")
    response2 = requests.post(
        'http://localhost:5000',
        b'name=john&email=doe\x80',
        headers={'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': str(len(b'\x80'))}
    )
    print("response with \x80 added to correctly formatted response in content: " + str(response2.status_code))
```

<!--
Describe the expected behavior that should have happened but didn't.
-->

Environment:

- Python version: 3.10.12
- Flask version: 3.0.3


Pull request:
restore invalid bytes behavior for form parser
#2694 removed max part limit from `application/x-www-form-urlencoded` parsing, previously added in #2620. However, it left the `except ValueError: raise RequestEntityTooLarge`. If `body.decode()` raised a `UnicodeError`, it resulted in a 413 error rather than being silently ignored. This restores that previous behavior.

fixes #2930

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 41878b5d..600ec0b2 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,10 @@ Version 3.0.4
 
 Unreleased
 
+-   Restore behavior where parsing `multipart/x-www-form-urlencoded` data with
+    invalid UTF-8 bytes in the body results in no form data parsed rather than a
+    413 error. :issue:`2930`
+
 
 Version 3.0.3
 -------------
diff --git a/src/werkzeug/formparser.py b/src/werkzeug/formparser.py
index ba84721e..e4b1f27f 100644
--- a/src/werkzeug/formparser.py
+++ b/src/werkzeug/formparser.py
@@ -281,15 +281,11 @@ class FormDataParser:
         ):
             raise RequestEntityTooLarge()
 
-        try:
-            items = parse_qsl(
-                stream.read().decode(),
-                keep_blank_values=True,
-                errors="werkzeug.url_quote",
-            )
-        except ValueError as e:
-            raise RequestEntityTooLarge() from e
-
+        items = parse_qsl(
+            stream.read().decode(),
+            keep_blank_values=True,
+            errors="werkzeug.url_quote",
+        )
         return stream, self.cls(items), self.cls()
 
 


Test patch:
diff --git a/tests/test_formparser.py b/tests/test_formparser.py
index 1ecb0120..ed63be68 100644
--- a/tests/test_formparser.py
+++ b/tests/test_formparser.py
@@ -122,13 +122,21 @@ class TestFormParser:
         req.max_form_parts = 1
         pytest.raises(RequestEntityTooLarge, lambda: req.form["foo"])
 
-    def test_x_www_urlencoded_max_form_parts(self):
+    def test_urlencoded_no_max(self) -> None:
         r = Request.from_values(method="POST", data={"a": 1, "b": 2})
         r.max_form_parts = 1
 
         assert r.form["a"] == "1"
         assert r.form["b"] == "2"
 
+    def test_urlencoded_silent_decode(self) -> None:
+        r = Request.from_values(
+            data=b"\x80",
+            content_type="application/x-www-form-urlencoded",
+            method="POST",
+        )
+        assert not r.form
+
     def test_missing_multipart_boundary(self):
         data = (
             b"--foo\r\nContent-Disposition: form-field; name=foo\r\n\r\n"