Skip to content

fix(screenshots): validate cached screenshot image bytes on read and write - #42120

Merged
eschutho merged 2 commits into
masterfrom
fix-screenshot-cache-validation
Jul 31, 2026
Merged

fix(screenshots): validate cached screenshot image bytes on read and write#42120
eschutho merged 2 commits into
masterfrom
fix-screenshot-cache-validation

Conversation

@eschutho

@eschutho eschutho commented Jul 16, 2026

Copy link
Copy Markdown
Member

SUMMARY

Dashboard/chart screenshot and thumbnail caching (ScreenshotCachePayload in superset/utils/screenshots.py) had two gaps:

  1. No read-side validation. BaseScreenshot.get_from_cache_key() returned whatever was in the cache as long as a status of UPDATED was recorded, even if the stored image was None/0-byte or otherwise not a real image. Since the cache key is digest-based, an unchanged dashboard/chart kept serving the same stale/blank entry indefinitely (e.g. a blank PDF download).
  2. Write-side validation was falsy-only. A prior fix (fix(screenshots): catch empty-bytes tiled result and set ERROR on falsy image #41097) set ERROR status when the screenshot task produced a falsy (None/b"") result, but a non-empty, non-image payload (e.g. truncated/corrupt bytes) still passed the if image: check and got cached with UPDATED status.

This PR adds a shared, cheap validator (validate_screenshot_image() — checks non-empty + PNG/JPEG magic-byte header, no full decode) used on both paths:

  • Read side: get_from_cache_key() now rejects a cached payload that claims a successful screenshot (status == UPDATED) but fails validation, returning None — the same value callers already treat as a cache miss — and logs a WARNING with the cache key and the reason (empty vs undecodable). Because both the dashboard and chart screenshot/thumbnail/cache_* endpoints already call this same shared classmethod, this closes the hole for both resource types without touching charts/api.py or dashboards/api.py.
  • Write side: BaseScreenshot.compute_and_cache() now runs the same check on the freshly generated image before caching it as a success. If it fails, the payload is marked ERROR (consistent with fix(screenshots): catch empty-bytes tiled result and set ERROR on falsy image #41097's approach) instead of UPDATED, and a WARNING with the cache key and reason is logged.

Scope and known limitation — what this does and does NOT protect against

This validates image bytes and headers only. It deliberately does not inspect image content:

  • A structurally valid but visually blank capture (e.g. an all-white PNG of an empty dashboard grid) passes this validation and will be cached and served.
  • A screenshot of charts stuck on loading spinners likewise passes — it is a perfectly well-formed PNG.

Preventing those is capture-side responsibility, and is where the readiness/budget work lives (#42253/#42427 positive readiness checks, #42273 no-unguarded-fallback, #42118 tiled wait budget, #42624 report deadline): with those in place, a blank/spinner capture fails the capture instead of ever reaching the cache. This PR is the complementary cache-layer guarantee for the class of corruption those fixes can't address: empty, truncated, or non-image bytes can never be cached as success nor served from cache — a failure class observed in production (0-byte cached assets) that, with digest-based keys and no TTL eviction in some deployments, previously poisoned an entry indefinitely. A pixel-level blank-image detector would be the durable catch-all for the remaining gap, but that's a separate, heavier change with genuine false-positive risk (legitimately near-empty dashboards) and is intentionally out of scope here.

Also explicitly not changed:

  • Capture/webdriver logic (superset/utils/screenshot_utils.py, superset/utils/webdriver.py) — untouched; owned by the readiness/budget PRs above.
  • Cache backend, cache keys, and TTLs — unchanged.
  • Behavior for correctly-cached images — a valid, non-empty PNG/JPEG payload is served and cached exactly as before; this only changes behavior for payloads that were already broken (empty/corrupt).
  • No new config flags — the validation is unconditional, since serving a 0-byte image is never the intended behavior.

TESTING INSTRUCTIONS

Added unit tests in tests/unit_tests/utils/test_screenshot_cache_fix.py and updated fixtures in tests/unit_tests/utils/screenshot_test.py (existing tests used non-image placeholder bytes like b"image_data" as stand-ins for a "successful" screenshot; these now use a minimal valid PNG header so they still exercise the success path under the new validation):

  • Read side: a cached payload with a 0-byte image is served as a cache miss (None) with a WARNING logged; a payload with non-image garbage bytes is likewise treated as a cache miss; a valid PNG-header payload is served normally; a non-UPDATED (e.g. PENDING) payload is returned as-is.
  • Write side: compute_and_cache() with an empty or garbage-bytes screenshot result caches ERROR status (never UPDATED) and logs a WARNING including the cache key and reason.

Run:

pytest tests/unit_tests/utils/test_screenshot_cache_fix.py tests/unit_tests/utils/screenshot_test.py -q

48 passed. Also ran the broader tests/unit_tests/utils/ -k screenshot suite (76 passed); 3 unrelated pre-existing failures in webdriver_test.py/test_screenshot_utils.py reproduce identically on master (playwright-version mismatch in the test environment, unrelated to this change and in files this PR does not touch).

ruff check passes on the changed files; mypy reports no errors in superset/utils/screenshots.py.

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

🤖 Generated with Claude Code

…write

Reject stale/empty/corrupt cached screenshot payloads instead of serving
them: get_from_cache_key now treats a payload that claims a successful
screenshot but has empty or non-image bytes as a cache miss, and
compute_and_cache applies the same cheap header check before marking a
result as cached-success, falling back to ERROR status otherwise.

Co-Authored-By: Claude <noreply@anthropic.com>
@dosubot dosubot Bot added the infra:caching Infra setup and configuration related to caching label Jul 16, 2026
@rusackas

Copy link
Copy Markdown
Member

Thanks Elizabeth! LGTM on the approach, and thanks for the tests.

One nit: since validate_screenshot_image already returns "empty" for falsy bytes, is the image and in compute_and_cache's check redundant, or am I missing something?

Will take another look once CI's green.

Copilot AI 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.

Pull request overview

This PR hardens screenshot/thumbnail caching by adding lightweight validation of cached screenshot bytes on both read and write paths in superset/utils/screenshots.py, preventing empty or non-image payloads from being served (or recorded as successful cache entries) indefinitely.

Changes:

  • Add validate_screenshot_image() (PNG/JPEG magic-byte + non-empty check) and apply it when reading cached payloads and when caching newly computed screenshots.
  • Treat invalid cached UPDATED payloads as cache misses (None) and emit a warning log with the cache key and invalid reason.
  • Update and extend unit tests to use minimal valid PNG-header bytes for “success” cases, plus new coverage for invalid read/write payload handling.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
superset/utils/screenshots.py Adds shared image-bytes validation and enforces it during cache reads and writes to avoid serving/caching invalid images as successful.
tests/unit_tests/utils/test_screenshot_cache_fix.py Updates fixtures to use valid PNG-header bytes and adds tests for read-side invalid cache rejection + write-side invalid payload handling and logging.
tests/unit_tests/utils/screenshot_test.py Updates legacy “plain bytes” cache test data to pass new validation and updates compute/caching fixtures accordingly.

Comment on lines +371 to +376
if invalid_reason:
logger.warning(
"Not caching screenshot result for %s: %s image payload",
cache_key,
invalid_reason,
)
@bito-code-review

Copy link
Copy Markdown
Contributor

The warning message can be updated to better reflect that an error payload is being cached, and the logic can be adjusted to avoid redundant warnings when the status is already set to ERROR. You can modify the else block in compute_and_cache to check the current status before logging the warning.

                else:
                    if invalid_reason and cache_payload.status != StatusValues.ERROR:
                        logger.warning(
                            "Caching error payload for %s: %s image payload",
                            cache_key,
                            invalid_reason,
                        )
                    if cache_payload.status != StatusValues.ERROR:
                        cache_payload.error()

superset/utils/screenshots.py

else:
                    if invalid_reason and cache_payload.status != StatusValues.ERROR:
                        logger.warning(
                            "Caching error payload for %s: %s image payload",
                            cache_key,
                            invalid_reason,
                        )
                    if cache_payload.status != StatusValues.ERROR:
                        cache_payload.error()

Answers review feedback: image and is redundant at runtime since
validate_screenshot_image only returns None for truthy, valid bytes,
but mypy can't infer that relationship across the function-call
boundary, so the guard stays for type narrowing.

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

netlify Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

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

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 14.81481% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.90%. Comparing base (b459601) to head (4800a88).
⚠️ Report is 190 commits behind head on master.

Files with missing lines Patch % Lines
superset/utils/screenshots.py 14.81% 23 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #42120      +/-   ##
==========================================
- Coverage   65.08%   64.90%   -0.18%     
==========================================
  Files        2752     2785      +33     
  Lines      154475   156870    +2395     
  Branches    35404    35791     +387     
==========================================
+ Hits       100544   101821    +1277     
- Misses      52019    53071    +1052     
- Partials     1912     1978      +66     
Flag Coverage Δ
hive 38.42% <14.81%> (-0.60%) ⬇️
mysql 57.62% <14.81%> (-0.13%) ⬇️
postgres 57.65% <14.81%> (-0.15%) ⬇️
presto 40.34% <14.81%> (-0.65%) ⬇️
python 59.07% <14.81%> (-0.18%) ⬇️
sqlite 57.28% <14.81%> (-0.14%) ⬇️
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.

@bito-code-review

bito-code-review Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #8be4c1

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/utils/screenshots.py - 1
    • Dead guard in conditional · Line 369-369
      The `image and` guard on line 369 is redundant — `validate_screenshot_image` already returns `None` only for truthy, well-formed bytes, so a non-None `invalid_reason` means the image is invalid. Removing it eliminates the dead guard and makes the mypy comment on lines 365–368 accurate. The narrowing is already guaranteed by `invalid_reason is None` alone.
Filtered by Review Rules

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

  • tests/unit_tests/utils/screenshot_test.py - 2
    • Missing type annotation on constant · Line 38-38
    • Missing type annotation on local variable · Line 99-99
Review Details
  • Files reviewed - 3 · Commit Range: f79afef..4800a88
    • superset/utils/screenshots.py
    • tests/unit_tests/utils/screenshot_test.py
    • tests/unit_tests/utils/test_screenshot_cache_fix.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

@eschutho
eschutho requested a review from rebenitez1802 July 25, 2026 00:12

@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 f6c574e into master Jul 31, 2026
61 checks passed
@eschutho
eschutho deleted the fix-screenshot-cache-validation branch July 31, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

infra:caching Infra setup and configuration related to caching preset-io size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants