Skip to content

fix(alerts-reports): catch CroniterBadDateError in report frequency validation - #42650

Merged
eschutho merged 1 commit into
masterfrom
fix-report-frequency-croniter-bad-date
Jul 31, 2026
Merged

fix(alerts-reports): catch CroniterBadDateError in report frequency validation#42650
eschutho merged 1 commit into
masterfrom
fix-report-frequency-croniter-bad-date

Conversation

@eschutho

Copy link
Copy Markdown
Member

SUMMARY

superset/reports/schemas.py::validate_crontab() only checks croniter.is_valid(str(value)), which is purely syntactic — it returns True for cron expressions that can never produce a real calendar date (e.g. 0 0 30 2 * for February 30th, or 0 0 31 4 * for April 31st).

When ALERT_MINIMUM_INTERVAL/REPORT_MINIMUM_INTERVAL is configured >= 120 seconds (a common operator setting to enforce a minimum report/alert interval), BaseReportScheduleCommand.validate_report_frequency() calls croniter(cron_schedule) and iterates it with next(schedule). For a never-matching crontab this raises croniter.croniter.CroniterBadDateError — whose MRO is (CroniterBadDateError, CroniterError, ValueError, Exception, BaseException, object), i.e. not a marshmallow ValidationError. Both CreateReportScheduleCommand.run() and UpdateReportScheduleCommand.run() wrap the call in except ValidationError, so the raw croniter exception propagates past that handler, uncaught, up to the API layer, producing an opaque 500 instead of a 422.

This is the same underlying croniter behavior fixed once already in #42486 (superset/tasks/cron_util.py::cron_schedule_window(), the celery-beat scheduler path — catch + log + skip), but this is a different site: synchronous API-layer validation during report/alert create/update, where the correct fix is to surface a proper validation error rather than silently skip.

Related prior fixes in this bug-class pipeline (raw system/library exceptions propagating instead of proper Superset/marshmallow validation errors): #42366, #42401, #42426, #42442, #42486.

FIX

  • Added ReportScheduleCrontabNotValidError (superset/commands/report/exceptions.py), a marshmallow ValidationError subclass on the crontab field, following the existing ReportScheduleFrequencyNotAllowed pattern.
  • Wrapped the croniter iteration in validate_report_frequency() (superset/commands/report/base.py) in try/except CroniterBadDateError, raising ReportScheduleCrontabNotValidError from the caught exception.
  • Added a regression test in tests/unit_tests/commands/report/base_test.py covering both ReportScheduleType.ALERT and ReportScheduleType.REPORT with the 0 0 30 2 * crontab.

TESTING INSTRUCTIONS

  • Confirmed empirically (before applying the fix) that croniter.is_valid("0 0 30 2 *") returns True in this environment's pinned croniter version, and that calling validate_report_frequency("0 0 30 2 *", ReportScheduleType.ALERT) with a minimum interval configured (e.g. 5 minutes) raised an uncaught CroniterBadDateError, not a ValidationError.
  • Applied the fix and confirmed the same call now raises ReportScheduleCrontabNotValidError (a ValidationError) instead.
  • Ran the full tests/unit_tests/commands/report/base_test.py file: 95 passed (93 pre-existing + 2 new, one per report type), no regressions.
  • Ran ruff check and ruff format --check on all changed files: all checks passed / already formatted.
  • Ran mypy on the changed files: no new errors introduced (one pre-existing, unrelated call-arg error on not_found_exc() at line 73 of base.py was confirmed present on unmodified HEAD as well).

To manually verify: configure ALERT_MINIMUM_INTERVAL = 300 (or any value >= 120), then attempt to create an alert with crontab 0 0 30 2 * via the API — before this fix, the request returns a 500; after, it returns a 422 with a clear "Invalid crontab schedule" message.

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

Tradeoffs: This is a new, additive validation error on a previously-uncaught crash path — no existing behavior changes for any crontab that currently passes. This code path only triggers for genuinely never-firing crontabs that admins would have needed to fix anyway; before this fix they got an opaque 500, after this fix they get a clear 422 telling them the crontab is invalid.

…alidation

Fixes an uncaught CroniterBadDateError in report/alert frequency validation.

marshmallow's validate_crontab only checks croniter.is_valid(), which is
purely syntactic and accepts crontabs that never produce a real calendar
date (e.g. "0 0 30 2 *" for February 30th). When ALERT_MINIMUM_INTERVAL or
REPORT_MINIMUM_INTERVAL is configured >= 120s, validate_report_frequency
iterates the schedule with croniter, which raises CroniterBadDateError
instead of a ValidationError, propagating past the existing except
ValidationError handling in CreateReportScheduleCommand and
UpdateReportScheduleCommand and surfacing as an opaque 500.
@dosubot dosubot Bot added the alert-reports Namespace | Anything related to the Alert & Reports feature label Jul 31, 2026
@bito-code-review

bito-code-review Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #285e51

Actionable Suggestions - 0
Review Details
  • Files reviewed - 3 · Commit Range: a452f7a..a452f7a
    • superset/commands/report/base.py
    • superset/commands/report/exceptions.py
    • tests/unit_tests/commands/report/base_test.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

@netlify

netlify Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit a452f7a
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a6cd0f3175b110008d9e31f
😎 Deploy Preview https://deploy-preview-42650--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.

Comment on lines +292 to +306
try:
schedule = croniter(cron_schedule)
current_exec = next(schedule)

for _i in range(iterations):
next_exec = next(schedule)
diff, current_exec = next_exec - current_exec, next_exec
if int(diff) < minimum_interval:
raise ReportScheduleFrequencyNotAllowed(
report_type=report_type, minimum_interval=minimum_interval
)
except CroniterBadDateError as ex:
raise ReportScheduleCrontabNotValidError(
cron_schedule=cron_schedule
) from ex

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: The never-matching-date check is only reached after the minimum_interval < 120 early return. With the default minimum interval of 0 (or any value below 120 seconds), schedules such as 0 0 30 2 * still pass schema validation and are accepted even though the scheduler can never produce an execution for them. Validate calendar validity independently of the frequency-limit threshold, or move this check before the early return. [incomplete implementation]

Severity Level: Major ⚠️
- ⚠️ Default zero-minute configuration bypasses calendar validation.

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/commands/report/base.py
**Line:** 292:306
**Comment:**
	*Incomplete Implementation: The never-matching-date check is only reached after the `minimum_interval < 120` early return. With the default minimum interval of `0` (or any value below 120 seconds), schedules such as `0 0 30 2 *` still pass schema validation and are accepted even though the scheduler can never produce an execution for them. Validate calendar validity independently of the frequency-limit threshold, or move this check before the early return.

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 valid. The current implementation performs the frequency check before the calendar validation, meaning if the frequency check returns early (e.g., due to a low minimum_interval), the calendar validation is skipped. To resolve this, the calendar validation should be performed independently of the frequency check, or the frequency check should be moved after the calendar validation.

Here is a concise fix to ensure calendar validation occurs regardless of the frequency check:

# superset/commands/report/base.py

        iterations = 60 if minimum_interval <= 3660 else 24
        try:
            schedule = croniter(cron_schedule)
            current_exec = next(schedule)
        except CroniterBadDateError as ex:
            raise ReportScheduleCrontabNotValidError(
                cron_schedule=cron_schedule
            ) from ex

        for _i in range(iterations):
            next_exec = next(schedule)
            diff, current_exec = next_exec - current_exec, next_exec
            if int(diff) < minimum_interval:
                raise ReportScheduleFrequencyNotAllowed(
                    report_type=report_type, minimum_interval=minimum_interval
                )

Would you like me to fetch all other comments on this PR to validate and implement fixes for them as well?

superset/commands/report/base.py

iterations = 60 if minimum_interval <= 3660 else 24
        try:
            schedule = croniter(cron_schedule)
            current_exec = next(schedule)
        except CroniterBadDateError as ex:
            raise ReportScheduleCrontabNotValidError(
                cron_schedule=cron_schedule
            ) from ex

        for _i in range(iterations):
            next_exec = next(schedule)
            diff, current_exec = next_exec - current_exec, next_exec
            if int(diff) < minimum_interval:
                raise ReportScheduleFrequencyNotAllowed(
                    report_type=report_type, minimum_interval=minimum_interval
                )

@eschutho
eschutho requested a review from rebenitez1802 July 31, 2026 16:53
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.71429% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.44%. Comparing base (96a12e0) to head (a452f7a).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
superset/commands/report/base.py 90.90% 1 Missing ⚠️
superset/commands/report/exceptions.py 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #42650      +/-   ##
==========================================
+ Coverage   55.88%   65.44%   +9.56%     
==========================================
  Files        2810     2810              
  Lines      159362   159368       +6     
  Branches    36372    36372              
==========================================
+ Hits        89055   104299   +15244     
+ Misses      69475    53027   -16448     
- Partials      832     2042    +1210     
Flag Coverage Δ
hive 38.09% <21.42%> (-0.01%) ⬇️
mysql 57.82% <85.71%> (?)
postgres 57.87% <85.71%> (?)
presto 39.98% <21.42%> (-0.01%) ⬇️
python 59.25% <85.71%> (+19.21%) ⬆️
sqlite 57.49% <85.71%> (?)
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.

@rebenitez1802 rebenitez1802 left a comment

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.

LGTM

@eschutho
eschutho merged commit 1bfbce3 into master Jul 31, 2026
69 checks passed
@eschutho
eschutho deleted the fix-report-frequency-croniter-bad-date branch July 31, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

alert-reports Namespace | Anything related to the Alert & Reports feature preset-io size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants