Repository: marshmallow-code/webargs

Issue:
Failing to (re)raise an error when handling validation errors should raise a new error
Per #525 , we're going to start warning if you setup an error handler which does not, itself, raise an error. The result of failing to raise in your handler is that parsing "falls through" and returns incorrect data (`None` today, but the behavior is not well-defined) to the caller.

In version 7.0 of webargs, we should change this to raise an error, because it's not correct usage of webargs.

I think the most developer-friendly thing for us to do in this case is to raise `ValueError` or something similar (i.e. not a `ValidationError`, which they might be catching and handling outside of the parsing context). That will make it immediately apparent to the developer that they've done something wrong.
We can't make any guesses about whether or not people are testing their error handlers well, so no matter what we do here, there's a risk that people's applications crashfail in these scenarios.


Discussion:

Resolving patch:
diff --git a/src/webargs/core.py b/src/webargs/core.py
--- a/src/webargs/core.py
+++ b/src/webargs/core.py
@@ -283,10 +283,9 @@ def parse(
                 error_status_code=error_status_code,
                 error_headers=error_headers,
             )
-            warnings.warn(
-                "_on_validation_error hook did not raise an exception and flow "
-                "of control returned to parse(). You may get unexpected results"
-            )
+            raise ValueError(
+                "_on_validation_error hook did not raise an exception"
+            ) from error
         return data
 
     def get_default_request(self):


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -289,14 +289,19 @@ def test_value_error_raised_if_parse_called_with_invalid_location(parser, web_re
 
 @mock.patch("webargs.core.Parser.handle_error")
 def test_handle_error_called_when_parsing_raises_error(handle_error, web_request):
+    # handle_error must raise an error to be valid
+    handle_error.side_effect = ValidationError("parsing failed")
+
     def always_fail(*args, **kwargs):
         raise ValidationError("error occurred")
 
     p = Parser()
     assert handle_error.call_count == 0
-    p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
+    with pytest.raises(ValidationError):
+        p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
     assert handle_error.call_count == 1
-    p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
+    with pytest.raises(ValidationError):
+        p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
     assert handle_error.call_count == 2
 
 
@@ -360,6 +365,25 @@ def handle_error(error, req, schema, *, error_status_code, error_headers):
         parser.parse(mock_schema, web_request)
 
 
+def test_custom_error_handler_must_reraise(web_request):
+    class CustomError(Exception):
+        pass
+
+    mock_schema = mock.Mock(spec=Schema)
+    mock_schema.strict = True
+    mock_schema.load.side_effect = ValidationError("parsing json failed")
+    parser = Parser()
+
+    @parser.error_handler
+    def handle_error(error, req, schema, *, error_status_code, error_headers):
+        pass
+
+    # because the handler above does not raise a new error, the parser should
+    # raise a ValueError -- indicating a programming error
+    with pytest.raises(ValueError):
+        parser.parse(mock_schema, web_request)
+
+
 def test_custom_location_loader(web_request):
     web_request.data = {"foo": 42}
 
diff --git a/tests/test_flaskparser.py b/tests/test_flaskparser.py
--- a/tests/test_flaskparser.py
+++ b/tests/test_flaskparser.py
@@ -1,4 +1,4 @@
-from werkzeug.exceptions import HTTPException
+from werkzeug.exceptions import HTTPException, BadRequest
 import pytest
 
 from flask import Flask
@@ -85,6 +85,9 @@ def test_parsing_unexpected_headers_when_raising(self, testapp):
 
 @mock.patch("webargs.flaskparser.abort")
 def test_abort_called_on_validation_error(mock_abort):
+    # error handling must raise an error to be valid
+    mock_abort.side_effect = BadRequest("foo")
+
     app = Flask("testapp")
 
     def validate(x):
@@ -97,7 +100,8 @@ def validate(x):
         data=json.dumps({"value": 41}),
         content_type="application/json",
     ):
-        parser.parse(argmap)
+        with pytest.raises(HTTPException):
+            parser.parse(argmap)
     mock_abort.assert_called()
     abort_args, abort_kwargs = mock_abort.call_args
     assert abort_args[0] == 422