Skip to content

fix(errors): map uncaught SupersetException status/log-level correctly - #42643

Open
eschutho wants to merge 2 commits into
masterfrom
sentry-12ez-query-validation-log-level
Open

fix(errors): map uncaught SupersetException status/log-level correctly#42643
eschutho wants to merge 2 commits into
masterfrom
sentry-12ez-query-validation-log-level

Conversation

@eschutho

@eschutho eschutho commented Jul 31, 2026

Copy link
Copy Markdown
Member

SUMMARY

Fixes SUPERSET-PYTHON-12EZQueryObjectValidationError: Error while rendering virtual dataset query: list object has no element 0, culprit ChartDataRestApi.data, 235,727 events (the highest-volume unresolved Sentry issue in our production org), 0 users impacted.

ROOT CAUSE

A customer virtual dataset SQL template does an indexed lookup such as filter_values('col')[0]. When no dashboard filter is active for that column, filter_values() returns [] and Jinja raises UndefinedError: list object has no element 0 during template rendering.

That's already correctly caught in get_rendered_sql() (superset/models/helpers.py) and re-raised as QueryObjectValidationError, a SupersetException subclass with status = 400 — this part of the flow is correct and untouched by this PR.

The actual bug: this exception leaks through a specific, unprotected call path. QueryContextProcessor.get_df_payload() (superset/common/query_context_processor.py) calls self.query_cache_key(query_obj) (line 93) before the method's only try/except block (which starts at line 116 and just wraps get_query_result/main query execution). When a template uses one of the macros matched by ExtraCache.regex (current_user_id, current_username, current_user_rls_rules, url_param, etc. — superset/jinja_context.py), query_cache_key() calls get_extra_cache_keys()get_sqla_query()get_from_clause()get_rendered_sql() to compute the cache key, all outside that try/except. So a QueryObjectValidationError raised here is never caught by get_df_payload, and ChartDataRestApi.data() (superset/charts/data/api.py) isn't decorated with @handle_api_exception either, so it propagates all the way to Flask's global catch-all handler (show_unexpected_exception in superset/views/error_handling.py). That handler unconditionally calls logger.exception(ex) (always ERROR + full traceback) and returns HTTP 500 via json_error_response's default status — regardless of the exception's actual status attribute.

This isn't unique to this one exception type: any SupersetException subclass that lacks its own specific @app.errorhandler (most of the validation-error types in superset/exceptions.pyQueryObjectValidationError, AdvancedDataTypeResponseError, InvalidPostProcessingError, CacheLoadError, NoDataException, NullValueException, SupersetTemplateException, DatabaseNotFound, MissingUserContextException, QueryClauseValidationException, ScreenshotImageNotAvailableException, etc.) hits this same catch-all whenever it isn't caught by a more specific view-level handler first.

Note: there's an unrelated in-flight PR (#42366) touching the same get_rendered_sql() function, but for a different (and, on inspection, redundant — jinja2.exceptions.UndefinedError is already a subclass of the TemplateError the existing except clause catches) fix. This PR doesn't touch that function and doesn't conflict with it.

FIX

Added @app.errorhandler(SupersetException) in set_app_error_handlers(), mirroring the existing (correct) per-view handle_api_exception pattern:

@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,
    )

get_logger_from_status maps 4xx → logger.warning, 5xx → logger.exception — the same mapping handle_api_exception already uses for decorated views. Subclasses with their own specific handler (SupersetErrorException, SupersetErrorsException, CommandException) are unaffected — Flask/Werkzeug dispatches by MRO distance, so those keep routing to their own handlers.

TRADEOFFS

This is a genuine behavior change, disclosed explicitly: for SupersetException subclasses with a non-default (non-500) status that previously reached this catch-all uncaught, HTTP status now reflects the real status (e.g. 400 instead of 500) and the log level drops from ERROR to WARNING for 4xx cases. This is more correct, but any API client currently branching on status == 500 for one of these previously-uncaught validation errors will see a different status code now. Subclasses that don't override status (default 500) are unaffected — still logged at ERROR and returned as 500 (covered by a regression test).

TESTING INSTRUCTIONS

  • tests/unit_tests/views/test_error_handling.py — new TestShowSupersetException class:
    • test_4xx_superset_exception_returns_its_status_and_logs_at_warning
    • test_5xx_superset_exception_still_returns_500_and_logs_at_error (no-regression case)
  • Full tests/unit_tests/views/ suite (183 tests), tests/unit_tests/jinja_context_test.py + tests/unit_tests/models/helpers_test.py (248 tests, confirms no overlap with fix(jinja): handle UndefinedError from virtual dataset templates #42366's area): all green.
  • ruff check / ruff format --check: clean.

Shortcut: https://app.shortcut.com/preset/story/115609

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

…y (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>
@dosubot dosubot Bot added the global:error Related to global errors affecting the platform label Jul 31, 2026
@bito-code-review

bito-code-review Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #52adf8

Actionable Suggestions - 0
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • superset/views/error_handling.py - 1
Review Details
  • Files reviewed - 2 · Commit Range: cfb0c6b..cfb0c6b
    • superset/views/error_handling.py
    • tests/unit_tests/views/test_error_handling.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment on lines +180 to +184
SupersetError(
message=ex.message,
error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
level=get_error_level_from_status(ex.status),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Content negotiation is bypassed for every SupersetException. Previously these exceptions reached show_unexpected_exception, which served Superset's 500.html page for non-debug browser requests accepting HTML; this handler always returns JSON, changing browser-facing error responses. Preserve the existing HTML behavior for HTML requests or route only API requests through the JSON response. [api mismatch]

Severity Level: Major ⚠️
- ⚠️ HTML clients receive JSON error payloads.
- ⚠️ Branded 500-page rendering is bypassed.
- ⚠️ Content negotiation differs across exception classes.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/views/error_handling.py
**Line:** 180:184
**Comment:**
	*Api Mismatch: Content negotiation is bypassed for every `SupersetException`. Previously these exceptions reached `show_unexpected_exception`, which served Superset's `500.html` page for non-debug browser requests accepting HTML; this handler always returns JSON, changing browser-facing error responses. Preserve the existing HTML behavior for HTML requests or route only API requests through the JSON response.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged issue is correct. The current implementation of show_superset_exception unconditionally returns a JSON response, which overrides Flask's default content negotiation and prevents the rendering of the standard HTML 500 error page for browser requests. To resolve this, you should check the request's accepted content types and fall back to the default error handler if HTML is preferred.

Here is a concise fix for superset/views/error_handling.py:

from flask import request

@app.errorhandler(SupersetException)
def show_superset_exception(ex: SupersetException) -> FlaskResponse:
    if request.accept_mimetypes.accept_html and not request.accept_mimetypes.accept_json:
        return show_unexpected_exception(ex)
    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,
    )

There are no other comments on this PR to address.

superset/views/error_handling.py

@app.errorhandler(SupersetException)
    def show_superset_exception(ex: SupersetException) -> FlaskResponse:
        if request.accept_mimetypes.accept_html and not request.accept_mimetypes.accept_json:
            return show_unexpected_exception(ex)
        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,
        )

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 18.18182% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.42%. Comparing base (7d2b184) to head (9701464).
⚠️ Report is 44 commits behind head on master.

Files with missing lines Patch % Lines
superset/views/error_handling.py 18.18% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #42643      +/-   ##
==========================================
- Coverage   65.44%   65.42%   -0.03%     
==========================================
  Files        2810     2810              
  Lines      159362   159442      +80     
  Branches    36372    36383      +11     
==========================================
+ Hits       104301   104319      +18     
- Misses      53019    53078      +59     
- Partials     2042     2045       +3     
Flag Coverage Δ
hive 38.07% <18.18%> (-0.02%) ⬇️
mysql 57.79% <18.18%> (-0.04%) ⬇️
postgres 57.83% <18.18%> (-0.04%) ⬇️
presto 39.96% <18.18%> (-0.03%) ⬇️
python 59.22% <18.18%> (-0.04%) ⬇️
sqlite 57.46% <18.18%> (-0.04%) ⬇️
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@eschutho
eschutho requested a review from gabotorresruiz July 31, 2026 15:26
The new show_superset_exception handler always returned JSON, but the
uncaught-exception path it replaces (show_unexpected_exception) served
the branded 500.html page for HTML-accepting browser requests. Add the
same text/html branch used by show_command_errors/show_http_exception
so browser clients keep getting the branded error page instead of a
raw JSON payload.

Co-Authored-By: Claude <noreply@anthropic.com>
@netlify

netlify Bot commented Aug 2, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 9701464
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a6f8cb258d3e4000854f014
😎 Deploy Preview https://deploy-preview-42643--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@bito-code-review

bito-code-review Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #5e393b

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: cfb0c6b..9701464
    • superset/views/error_handling.py
    • tests/unit_tests/views/test_error_handling.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

global:error Related to global errors affecting the platform preset-io size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant