Skip to content

fix(screenshot): surface real render errors, refcount CORS rule, bound webp re-encode#130

Merged
ronaldeddings merged 3 commits into
Hacker-Valley-Media:mainfrom
atabisz:port/screenshot-fallback-fixes
Jul 5, 2026
Merged

fix(screenshot): surface real render errors, refcount CORS rule, bound webp re-encode#130
ronaldeddings merged 3 commits into
Hacker-Valley-Media:mainfrom
atabisz:port/screenshot-fallback-fixes

Conversation

@atabisz

@atabisz atabisz commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Three fixes to the DOM-render screenshot path, found while running screenshots against large real-world pages on Windows.

What's here

1. Surface the real render error instead of "undefined"

html-to-image can reject with a raw DOM error Event — an <img>/SVG onerror when the serialized, resource-embedded SVG fails to decode on a large page. A DOM Event has no .message, so (err as Error).message rendered the literal string dom render failed: undefined, which tells you nothing.

describeRenderError coerces every error shape (Error, DOM Event, string, opaque object) into a real message, and sanitizes after coercion so a thrown literal "undefined"/"null"/"[object Object]" can't leak either. An <img> failure now reads image load failed (error on <img>) — the rendered SVG could not be decoded, likely too large or a resource blocked.

2. Refcount the CORS DNR rule

The screenshot CORS rule id is keyed on tabId, so two concurrent screenshots of the same tab share one rule. Whichever finishes first tore the rule down while the other was still fetching subresources — that render lost Access-Control-Allow-Origin: * mid-flight and tainted or failed.

installScreenshotCorsRule/uninstallScreenshotCorsRule now keep a per-tab refcount: install adds the rule on the first acquire, uninstall removes it only when the last concurrent operation releases. A failed install rolls the count back so it leaves no phantom reference (callers place install outside their try/finally, so a throw there means uninstall never runs).

3. Bound the webp re-encode

The render is already guarded by DOM_RENDER_TIMEOUT_MS, but the post-render webp re-encode wasn't. A large image that re-encodes slowly could hang past the CLI's timeout with no diagnostic. The re-encode now uses the same 30s guard and returns an actionable error.

Tests

  • describeRenderError — Error / DOM Event / <img> Event / string / bare primitive coercion, and that no shape leaks undefined.
  • CORS refcount — single install/uninstall, concurrent same-tab (rule survives until the last release), independent tabs, re-acquire after full release, and failed-install rollback.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed screenshot CORS rule lifecycle by reference-counting per tab, preventing premature removal during overlapping captures.
    • Added a bounded timeout for DOM-render WebP re-encoding to fail clearly when it stalls.
    • Improved DOM-render region screenshot rasterization by cropping during rasterization and adding stronger output validation plus consistent error messages.
  • Tests
    • Added Bun tests for DOM-render error formatting and for screenshot CORS per-tab refcount behavior, including failure/rollback and concurrency cases.
    • Added Bun tests covering rasterize output guard behavior for oversized/near-empty results.

…d webp re-encode

Three fixes to the DOM-render screenshot path.

- dom-screenshot: the render can reject with a raw DOM Event (an <img>/SVG
  onerror when the embedded SVG fails to decode on a large page). A DOM Event
  has no .message, so (err as Error).message rendered the literal 'undefined'.
  describeRenderError coerces every error shape into a real message and
  sanitizes after coercion, so 'undefined'/'null'/'[object Object]' can't leak.

- screenshot-cors: the CORS DNR rule id is keyed on tabId, so two concurrent
  screenshots of the same tab share one rule. The first to finish tore it down
  under the second, dropping ACAO:* mid-fetch and failing that render. Add a
  per-tab refcount: install on the first acquire, remove only when the last
  releases. A failed install rolls the count back so it leaves no phantom ref.

- screenshot: bound the post-render webp re-encode with the same 30s timeout
  the render already uses, so a large image that re-encodes slowly can't hang
  past the CLI ceiling.

Tests: describeRenderError coercion (Error/Event/string/primitive), CORS
refcount (single/concurrent/independent-tabs/re-acquire/failed-install).
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab48a940-3ee0-4a31-a67e-64dea45677ba

📥 Commits

Reviewing files that changed from the base of the PR and between 45efca9 and 8744358.

📒 Files selected for processing (4)
  • extension/dist-mv2/background-electron.js
  • extension/dist-mv2/content.js
  • extension/src/content/dom-screenshot.ts
  • test/dom-screenshot-rasterize-guard.test.ts

📝 Walkthrough

Walkthrough

The PR adds per-tab CORS rule reference counting, bounds WebP re-encoding with a timeout, and updates DOM screenshot rasterization, output validation, and error formatting. Tests cover the CORS lifecycle and render-error/output-guard behavior.

Changes

Screenshot reliability improvements

Layer / File(s) Summary
Per-tab CORS rule refcounting
extension/src/background/capabilities/screenshot-cors.ts, test/screenshot-cors-refcount.test.ts, extension/dist-mv2/background-electron.js
A corsRuleRefcount map tracks concurrent acquires per tabId; the DNR rule is installed only on 0→1 transition and removed only on last release, with rollback on install failure and tests covering single/multi-tab and rollback scenarios.
WebP re-encode timeout
extension/src/background/capabilities/screenshot.ts, extension/dist-mv2/background-electron.js
The DOM-render WebP re-encode step is wrapped with withCaptureTimeout, returning a distinct timeout failure message instead of a generic re-encode error.
DOM rasterization and output validation
extension/src/content/dom-screenshot.ts, extension/dist-mv2/content.js, test/dom-screenshot-rasterize-guard.test.ts
Region cropping moves into nativeRenderToDataUrl, raster output is validated with checkRasterizeOutput, and handleDomScreenshot now derives crop dimensions before returning or failing.
DOM render error normalization
extension/src/content/dom-screenshot.ts, test/dom-screenshot-error-describe.test.ts
New exported describeRenderError coerces Error, Event, string, and other thrown values into a meaningful, non-empty error message used by handleDomScreenshot's catch block, validated by a dedicated test file.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the three main screenshot fixes in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/screenshot-cors-refcount.test.ts (1)

91-103: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider adding a test for a concurrent acquirer during a failing install.

This suite covers rollback only for a single, sequential acquirer. Given the race flagged in screenshot-cors.ts (a second installScreenshotCorsRule call arriving while the first is still installing, then the first fails), a test simulating that interleaving would catch the reported bug and guard against regression once fixed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/screenshot-cors-refcount.test.ts` around lines 91 - 103, The rollback
coverage in installScreenshotCorsRule only checks a single failed acquire, but
it misses the concurrent interleaving where a second caller arrives while the
first install is still in progress. Add a test in
screenshot-cors-refcount.test.ts that exercises two overlapping
installScreenshotCorsRule calls, forces the first install path to fail, and
verifies the refcount and session rule state are fully rolled back for both
callers so no phantom reference remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@extension/src/background/capabilities/screenshot-cors.ts`:
- Around line 65-94: The shared refcount in
installScreenshotCorsRule/uninstallScreenshotCorsRule can be dropped by a failed
first updateSessionRules() while another same-tab acquire is still active.
Change the rollback in installScreenshotCorsRule so it only undoes the increment
for the current acquire (preserving any concurrent acquires in
corsRuleRefcount), and make uninstallScreenshotCorsRule continue to remove the
rule only when the last reference is released. Add or update a test around
overlapping installs of installScreenshotCorsRule to verify a failed install
does not clear another caller’s refcount entry.

---

Nitpick comments:
In `@test/screenshot-cors-refcount.test.ts`:
- Around line 91-103: The rollback coverage in installScreenshotCorsRule only
checks a single failed acquire, but it misses the concurrent interleaving where
a second caller arrives while the first install is still in progress. Add a test
in screenshot-cors-refcount.test.ts that exercises two overlapping
installScreenshotCorsRule calls, forces the first install path to fail, and
verifies the refcount and session rule state are fully rolled back for both
callers so no phantom reference remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a224488-5d38-47dc-80a1-57d85a331165

📥 Commits

Reviewing files that changed from the base of the PR and between c86a92d and 83db825.

📒 Files selected for processing (5)
  • extension/src/background/capabilities/screenshot-cors.ts
  • extension/src/background/capabilities/screenshot.ts
  • extension/src/content/dom-screenshot.ts
  • test/dom-screenshot-error-describe.test.ts
  • test/screenshot-cors-refcount.test.ts

Comment thread extension/src/background/capabilities/screenshot-cors.ts
atabisz and others added 2 commits July 5, 2026 08:41
…stall

Addresses the concurrency edge flagged in review. When a same-tab acquire
arrives while the first install's updateSessionRules() is still in flight and
that first install then fails, the rollback used delete(tabId), which wiped the
whole refcount entry — stranding the concurrent acquire with no live rule and
no reference, so a later uninstall's accounting could remove the rule under an
active operation.

Roll back only this acquire's increment: decrement by one, delete only if that
leaves zero. Add a test that interleaves two same-tab installs with a failing
first install and asserts the second acquire's reference survives (fails
against the old delete-the-entry rollback).
…rasterize time

Live testing PR Hacker-Valley-Media#130 on macOS surfaced the bug it missed: a page past the
browser's canvas limits (e.g. 300,000px tall) returned success with
size: 0 — toDataURL() silently yields "data:," when the backing store
fails to allocate. checkRasterizeOutput now turns that into an actionable
error naming --region / --target-max-long-edge / --scale.

To make --region a real remedy on such pages, region mode now crops at
rasterize time: the canvas is allocated at the crop size and the decoded
SVG is drawn offset into it. Decoding a huge SVG image succeeds where the
full-page canvas fails, so region captures work far past the limits — and
the post-render cropDataUrl re-encode step is gone entirely.

PRD-123. Live-verified: huge page → actionable error; --region on the
same page → valid 2560x4000 capture; full/region/element/webp parity on
normal pages; concurrent same-tab race harness still clean.

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

@ronaldeddings ronaldeddings left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed statically + dynamically (PRD-123): 14/14 PR tests pass, live race harness clean, maintainer follow-up adds the oversized-render guard + raster-time region crop. Merging with contributor commits intact.

@ronaldeddings ronaldeddings merged commit af3666e into Hacker-Valley-Media:main Jul 5, 2026
1 of 2 checks passed
@ronaldeddings

Copy link
Copy Markdown
Collaborator

Thanks @atabisz! Last week I ripped out html-to-image in favor of a custom implementation that was more resilient. Your PR helped catch a silent edge case that I missed. Great work and thank you!! 💪

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants