Repository: greyli/apiflask (python)

Issue:
Apply custom error processor for generic HTTP errors even `json_errors` is `False`
Currently, if the `json_errors` is `False` when creating the app instance, the error processor registered with `app.error_processor` will not be applied on generic HTTP errors. I think it's better to change this behavior to match the behavior of `auth.error_processor` and keep it explicit.

Pull request description:
Always apply custom error processor to HTTP errors
Even when json_error is set to False.

- fixes #171

<!--
If needed, ensure each step in the checklist below is complete. If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the `docs` folder and in code docstring.
- [x] Add an entry in `CHANGES.md` summarizing the change and linking to the issue and your username.
- [x] Add `*Version changed*` or `*Version added*` note in any relevant docs and docstring.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 58fbf33..2d45694 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -24,6 +24,8 @@ Released: --
     be included in the returned data.
 - Allow to use `json_or_form` location and fields (`DelimitedList` and `DelimitedTuple`)
   from webargs ([issue #254][issue_254]).
+- When creating a custom error processor, call it for generic HTTP errors even if the
+  `json_errors` is set to `False` when creating the app instance ([issue #171][issue_171]).
 
 [issue_211]: https://github.com/greyli/apiflask/issues/211
 [issue_231]: https://github.com/greyli/apiflask/issues/231
@@ -31,6 +33,7 @@ Released: --
 [issue_237]: https://github.com/greyli/apiflask/issues/237
 [issue_123]: https://github.com/greyli/apiflask/issues/123
 [issue_254]: https://github.com/greyli/apiflask/issues/254
+[issue_171]: https://github.com/greyli/apiflask/issues/171
 
 
 ## Version 0.12.0
diff --git a/src/apiflask/app.py b/src/apiflask/app.py
index 84670bd..6870fa5 100644
--- a/src/apiflask/app.py
+++ b/src/apiflask/app.py
@@ -358,19 +358,22 @@ class APIFlask(APIScaffold, Flask):
             return self.error_callback(error)
 
         if self.json_errors:
-            @self.errorhandler(WerkzeugHTTPException)  # type: ignore
-            def handle_werkzeug_errors(
-                e: WerkzeugHTTPException
-            ) -> ResponseReturnValueType:
-                headers = dict(e.get_headers())
-                # remove the original MIME header
-                del headers['Content-Type']
-                error = HTTPError(
-                    e.code,
-                    message=e.name,
-                    headers=headers
-                )
-                return self.error_callback(error)
+            self._apply_error_callback_to_werkzeug_errors()
+
+    def _apply_error_callback_to_werkzeug_errors(self) -> None:
+        @self.errorhandler(WerkzeugHTTPException)  # type: ignore
+        def handle_werkzeug_errors(
+            e: WerkzeugHTTPException
+        ) -> ResponseReturnValueType:
+            headers = dict(e.get_headers())
+            # remove the original MIME header
+            del headers['Content-Type']
+            error = HTTPError(
+                e.code,
+                message=e.name,
+                headers=headers
+            )
+            return self.error_callback(error)
 
     def dispatch_request(self) -> ResponseReturnValueType:  # type: ignore
         """Overwrite the default dispatch method in Flask.
@@ -443,18 +446,15 @@ class APIFlask(APIScaffold, Flask):
 
         The decorated callback function will be called in the following situations:
 
-        - Any HTTP exception is raised by Flask when `APIFlask(json_errors=True)` (default).
+        - Any HTTP exception is raised by Flask when handling request.
         - A validation error happened when parsing a request.
         - An exception triggered with [`HTTPError`][apiflask.exceptions.HTTPError]
         - An exception triggered with [`abort`][apiflask.exceptions.abort].
 
-        If you have set the `json_errors` argument to `True` when creating the `app`
-        instance, this callback function will also be used for normal HTTP errors,
-        for example, 404 and 500 errors, etc. You can still register a specific error
-        handler for a specific error code or exception with the
-        `app.errorhandler(code_or_exection)` decorator, in that case, the return
-        value of the error handler will be used as the response when the corresponding
-        error or exception happened.
+        You can still register a specific error handler for a specific error code
+        or exception with the `app.errorhandler(code_or_exection)` decorator,
+        in that case, the return value of the specific error handler will be used as the
+        response when the corresponding error or exception happened.
 
         The callback function must accept an error object as argument and return a valid
         response.
@@ -517,6 +517,11 @@ class APIFlask(APIScaffold, Flask):
         the detailed information about the validation error when the validation error
         happened.
 
+        *Version changed: 1.0*
+
+        - Apply this error processor to normal HTTP errors even when
+          `json_error` is set to `False` when creating `APIFlask` instance.
+
         *Version changed: 0.7.0*
 
         - Support registering an async callback function.
@@ -525,6 +530,7 @@ class APIFlask(APIScaffold, Flask):
             self.error_callback = self.ensure_sync(f)
         else:  # pragma: no cover
             self.error_callback = f
+        self._apply_error_callback_to_werkzeug_errors()
         return f
 
     def _register_openapi_blueprint(self) -> None:


Test patch:
diff --git a/tests/test_app.py b/tests/test_app.py
index 5649414..bffbbc1 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -139,6 +139,20 @@ def test_error_callback(app, client):
     assert rv.json['message'] == 'something was wrong'
 
 
+def test_apply_custom_error_callback_for_generic_errors():
+    app = APIFlask(__name__, json_errors=False)
+    client = app.test_client()
+
+    @app.error_processor
+    def custom_error_handler(e):
+        return {'message': 'custom handler'}, e.status_code
+
+    rv = client.get('/not-exist')
+    assert rv.status_code == 404
+    assert 'message' in rv.json
+    assert rv.json['message'] == 'custom handler'
+
+
 def test_skip_raw_blueprint(app, client):
     raw_bp = Blueprint('raw', __name__)
     api_bp = APIBlueprint('api', __name__, tag='test')