From 27aa8c0bb1cb453753ccf0cde05cfb56aeaae652 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:05:26 +0000 Subject: [PATCH 1/2] Implement flask_error_handler: error statuses and WWW-Authenticate for Flask apps (#37) Co-Authored-By: Claude Sonnet 5.5 --- py2http/default_configs.py | 8 ++++++- py2http/tests/test_error_statuses.py | 34 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/py2http/default_configs.py b/py2http/default_configs.py index f0c51f9..afe17f8 100644 --- a/py2http/default_configs.py +++ b/py2http/default_configs.py @@ -150,7 +150,13 @@ def bottle_error_handler(error: Exception): def flask_error_handler(error: Exception): - raise NotImplementedError() + """The ``(body, status, headers)`` a Flask view returns for an error, mapped like + the bottle handler (``OPAQUE_ERRORS`` hides the text of a 500).""" + message = str(error) + status = error_status(error) + if status == 500 and os.getenv("OPAQUE_ERRORS", None): + message = "Internal server error" + return {"error": message}, status, _status_headers(status) def default_error_handler(error: Exception): diff --git a/py2http/tests/test_error_statuses.py b/py2http/tests/test_error_statuses.py index ad43571..9de4af7 100644 --- a/py2http/tests/test_error_statuses.py +++ b/py2http/tests/test_error_statuses.py @@ -63,3 +63,37 @@ def test_aiohttp_error_handler(error, status): assert caught.value.status == status if status == 500: assert "internal detail" not in caught.value.text + + +@pytest.mark.parametrize("error, status", CASES) +def test_flask_error_handler(error, status, monkeypatch): + flask = pytest.importorskip("flask") + monkeypatch.delenv("PY2HTTP_WWW_AUTHENTICATE", raising=False) + monkeypatch.setenv("PY2HTTP_FRAMEWORK", "flask") + from py2http.default_configs import default_error_handler + + app = flask.Flask(__name__) + + @app.route("/fail") + def fail(): + return default_error_handler(error) + + response = app.test_client().get("/fail") + assert response.status_code == status + assert set(response.get_json()) == {"error"} + if status == 401: + assert response.headers["WWW-Authenticate"] == "Bearer" + else: + assert "WWW-Authenticate" not in response.headers + if status == 500: + assert "internal detail" in response.get_json()["error"] + + +def test_flask_opaque_errors_hides_500_text(monkeypatch): + flask = pytest.importorskip("flask") + monkeypatch.setenv("OPAQUE_ERRORS", "1") + from py2http.default_configs import flask_error_handler + + app = flask.Flask(__name__) + app.add_url_rule("/f", "f", lambda: flask_error_handler(KeyError("internal detail"))) + assert app.test_client().get("/f").get_json() == {"error": "Internal server error"} From a460be7c3b65dbf4d53b13d56efd26f42b28e1c6 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:06:38 +0000 Subject: [PATCH 2/2] flask_error_handler: 400 status line names the error, like bottle (#37) Co-Authored-By: Claude Sonnet 5.5 --- py2http/default_configs.py | 3 ++- py2http/tests/test_error_statuses.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/py2http/default_configs.py b/py2http/default_configs.py index afe17f8..baab228 100644 --- a/py2http/default_configs.py +++ b/py2http/default_configs.py @@ -156,7 +156,8 @@ def flask_error_handler(error: Exception): status = error_status(error) if status == 500 and os.getenv("OPAQUE_ERRORS", None): message = "Internal server error" - return {"error": message}, status, _status_headers(status) + status_line = f"400 {type(error).__name__}" if status == 400 else status + return {"error": message}, status_line, _status_headers(status) def default_error_handler(error: Exception): diff --git a/py2http/tests/test_error_statuses.py b/py2http/tests/test_error_statuses.py index 9de4af7..e71e9ef 100644 --- a/py2http/tests/test_error_statuses.py +++ b/py2http/tests/test_error_statuses.py @@ -81,6 +81,8 @@ def fail(): response = app.test_client().get("/fail") assert response.status_code == status assert set(response.get_json()) == {"error"} + if status == 400: # as the bottle handler: the status line names the error + assert response.status == f"400 {type(error).__name__}" if status == 401: assert response.headers["WWW-Authenticate"] == "Bearer" else: