Compare commits

...

1 Commits

Author SHA1 Message Date
Elizabeth Thompson
cfb0c6bd74 fix(errors): map uncaught SupersetException status/log-level correctly (SC-115609)
ChartDataRestApi.data() isn't decorated with @handle_api_exception, so a
correctly-classified QueryObjectValidationError (e.g. from a virtual
dataset's Jinja template indexing into an empty filter_values() list)
propagates uncaught to Flask's global catch-all, which always logs at
ERROR and returns HTTP 500 regardless of the exception's real status.

Add @app.errorhandler(SupersetException) mirroring the existing
handle_api_exception pattern: log level and HTTP status now follow
ex.status via get_logger_from_status, instead of being hardcoded to
ERROR/500. Subclasses with their own specific handler (SupersetErrorException,
SupersetErrorsException, CommandException) are unaffected by Flask's MRO
dispatch. 500-status SupersetException subclasses keep logging at ERROR.

Fixes SUPERSET-PYTHON-12EZ

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 15:19:10 +00:00
2 changed files with 69 additions and 0 deletions

View File

@@ -171,6 +171,21 @@ def set_app_error_handlers(app: Flask) -> None: # noqa: C901
logger.warning("SupersetErrorsException", exc_info=True)
return json_error_response(ex.errors, status=ex.status)
@app.errorhandler(SupersetException)
def show_superset_exception(ex: SupersetException) -> FlaskResponse:
logger_func, _ = get_logger_from_status(ex.status)
logger_func(ex.message, exc_info=True)
return json_error_response(
[
SupersetError(
message=ex.message,
error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
level=get_error_level_from_status(ex.status),
),
],
status=ex.status,
)
@app.errorhandler(CSRFError)
def refresh_csrf_token(ex: CSRFError) -> FlaskResponse:
"""Redirect to login if the CSRF token is expired"""

View File

@@ -23,6 +23,7 @@ from flask import Flask, Response
from flask_babel import Babel
from superset.errors import SupersetErrorType
from superset.exceptions import QueryObjectValidationError, SupersetException
from superset.superset_typing import FlaskResponse
from superset.utils import json
from superset.views.error_handling import handle_api_exception, set_app_error_handlers
@@ -110,3 +111,56 @@ class TestShowUnexpectedException:
== SupersetErrorType.GENERIC_BACKEND_ERROR.value
)
assert any(record.levelno >= logging.ERROR for record in caplog.records)
class TestShowSupersetException:
def _build_app_with_handlers(self) -> Flask:
# A fresh, minimal Flask app per test: `set_app_error_handlers` can
# only register handlers before the app has served its first
# request, so it can't share the module-scoped `app` fixture across
# tests in this class.
test_app = Flask(__name__)
test_app.config["DEBUG"] = False
Babel(test_app)
set_app_error_handlers(test_app)
@test_app.route("/query-validation-error")
def query_validation_error_view() -> FlaskResponse:
raise QueryObjectValidationError("list object has no element 0")
@test_app.route("/generic-superset-exception")
def generic_superset_exception_view() -> FlaskResponse:
raise SupersetException("boom")
return test_app
def test_4xx_superset_exception_returns_its_status_and_logs_at_warning(
self, caplog: pytest.LogCaptureFixture
):
client = self._build_app_with_handlers().test_client()
with caplog.at_level(logging.WARNING):
response = client.get("/query-validation-error")
assert response.status_code == 400
payload = json.loads(response.data)
assert payload["errors"][0]["message"] == "list object has no element 0"
assert not any(record.levelno >= logging.ERROR for record in caplog.records)
assert any(
record.levelno == logging.WARNING
and record.message == "list object has no element 0"
for record in caplog.records
)
def test_5xx_superset_exception_still_returns_500_and_logs_at_error(
self, caplog: pytest.LogCaptureFixture
):
client = self._build_app_with_handlers().test_client()
with caplog.at_level(logging.WARNING):
response = client.get("/generic-superset-exception")
assert response.status_code == 500
payload = json.loads(response.data)
assert payload["errors"][0]["message"] == "boom"
assert any(record.levelno >= logging.ERROR for record in caplog.records)