Skip to content

feat(engine): surface escape hatches in page.goto Nav timeout errors#2506

Merged
vanceingalls merged 1 commit into
mainfrom
via/darwin-goto-nav-timeout-hint
Jul 16, 2026
Merged

feat(engine): surface escape hatches in page.goto Nav timeout errors#2506
vanceingalls merged 1 commit into
mainfrom
via/darwin-goto-nav-timeout-hint

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

What

Wraps main-render Puppeteer page.goto errors matching Navigation timeout / net::ERR_TIMED_OUT with an augmented message that names:

  • The effective timeout currently applied (cfg.pageNavigationTimeout).
  • The env that raises it: PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS (ms).
  • The CLI flag that raises it: --browser-timeout (seconds).
  • The browser-binary escape hatch: HYPERFRAMES_BROWSER_PATH.
  • The field-signal shape: darwin/arm64 + CSS 3D + audio compound Docker hint — gated on all three inputs being explicitly true; falls back to generic hints when any input is unknown (see fallback notes below).

Non-matching errors flow through unchanged (returned as the same Error instance). Original error preserved via err.cause. Sibling of the just-merged augmentProtocolTimeoutError (#2504) — mutually exclusive regex classes, so the two augmenters never both fire on the same error.

Why

Field signal ts=1784146416 (#hyperframes-cli-feedback, darwin/arm64, HyperFrames CLI 0.7.58, severity 7/10):

Host render browser initialization timed out twice at page.goto with exact error: Navigation timeout of 60000 ms exceeded. Trigger: CSS 3D composition on macOS arm64 with an audio track. Baking the audio volume fade removed the scripted-audio probe, but host screenshot capture still stalled at 0/180 frames. Docker render succeeded and produced correct 1920x1080 H.264/AAC output.

The knobs already exist:

  • PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS env, default 60000 ms (packages/engine/src/config.tspageNavigationTimeout, env fallback wired in the same file).
  • --browser-timeout <seconds> CLI flag on hyperframes render (packages/cli/src/commands/render.ts — resolved via resolveBrowserTimeoutMsArg).
  • HYPERFRAMES_BROWSER_PATH env, points at a system Chrome / chrome-headless-shell binary (packages/cli/src/browser/manager.ts:77, surfaced on download-time failure by PR fix(cli): surface HYPERFRAMES_BROWSER_PATH hint on pinned browser download failures #2443).
  • Docker fallback path (hyperframes render ... --docker) that the field signal confirmed produced correct output on this exact composition.

Discoverability was the gap: Puppeteer's stock Navigation timeout of 60000 ms exceeded names none of these levers, and neither does HyperFrames' existing [FrameCapture:ERROR] page.goto failed ... diagnostic. Augmenting the error message closes the gap without changing runtime behaviour.

Mirrors the surfacing pattern from #2443 (download-time HYPERFRAMES_BROWSER_PATH hint on pinned-browser install failures), applied at the runtime page.goto layer.

How

  • New helper: packages/engine/src/services/pageNavigationTimeoutErrorHint.ts exports augmentPageNavigationTimeoutError(err, effectiveTimeoutMs, context?): Error and isPageNavigationTimeoutError(err): boolean. Conservative regex match — non-matching errors return the same instance unchanged.
  • Wired into renderOrchestrator.executeRenderJob's top-level catch block, composed after augmentProtocolTimeoutError. Reads cfg.pageNavigationTimeout.
  • Exported from @hyperframes/engine's public surface alongside the sibling protocol-timeout hint.

Compound-hint fallback (documented per stack-review guardrails)

The field signal cites the darwin/arm64 + CSS 3D + audio compound as the shape where Docker rendered identically. The Docker hint therefore fires ONLY when all three flags are explicitly true. When any flag is unknown (undefined) the helper falls back to the generic env + browser-path hints — the Docker fallback isn't universally applicable, and surfacing it outside the known-good compound risks recommending it on shapes it hasn't been verified for.

At the current wire-up in renderOrchestrator.executeRenderJob's top-level catch:

  • hasAudio: computed in the audio_process stage but block-scoped inside the try — not visible in the catch. Left as undefined.
  • hasCss3D: no compile-time signal is currently threaded through the render pipeline (grep packages/producer/src/services/ and packages/engine/src/services/ — no hasCss3D boolean exists; parseTransformMatrix in alphaBlit.ts detects 3D matrices at engine-init runtime, after page.goto has already succeeded). Left as undefined.

So in production today the Docker hint does not fire — reporters on darwin/arm64 hitting Nav timeout get the env + browser-path hints, which alone represent 3 escape hatches the reporter didn't have before. When a future PR lands a compile-time hasCss3D scan (e.g. htmlCompiler.ts pass over transform-style: preserve-3d, perspective:, rotateX(, rotateY(, matrix3d() and hoists hasAudio above the try boundary, threading both flags here enables the full compound Docker hint without touching this helper's signature.

Design notes

  • Conservative match: the regex /Navigation timeout|net::ERR_TIMED_OUT/i only fires on the exact strings Puppeteer emits for page.goto failures. Memory-exhaustion errors, protocol timeouts, and other CDP errors flow through unchanged.
  • Non-Error inputs: coerced with new Error(String(err)) so callers always receive a well-typed Error, but the augmentation never fires (coerced strings can't match without going through err.message).
  • err.cause preserved: downstream logging / observability that inspects .cause still sees the raw Puppeteer message.
  • Mutually exclusive with augmentProtocolTimeoutError: protocol-timeout regex matches Runtime.callFunctionOn timed out|Target closed|protocolTimeout, this one matches Navigation timeout|net::ERR_TIMED_OUT. No overlap, so composing them at the catch site is safe.
  • String-form stays the compatibility path: augmented .message is what normalizeErrorMessage extracts, which is what publishRenderFailure publishes. errorDetails continues to consume the original error, so instanceof checks downstream are unaffected.

Test plan

  • Unit tests added — packages/engine/src/services/pageNavigationTimeoutErrorHint.test.ts covers:

    • Non-Navigation-timeout errors pass through unchanged (same instance).
    • Navigation timeout of Xms exceeded messages are augmented with the effective timeout.
    • Env + CLI + browser-path hints all present in the generic augmentation.
    • err.cause preserved on the augmented error.
    • net::ERR_TIMED_OUT matches.
    • Non-Error thrown values coerced without augmenting.
    • Docker hint fires only when all three (darwin, arm64, CSS 3D, audio) match.
    • Docker hint suppressed on non-darwin platforms.
    • Docker hint suppressed on darwin/x64 (Intel Macs).
    • Docker hint suppressed on darwin/arm64 without CSS 3D.
    • Docker hint suppressed on darwin/arm64 without audio.
    • Docker hint suppressed when CSS 3D / audio inputs are unknown (locks the fallback documented above).
    • Platform/arch defaults to the current process when context omits them.
    • isPageNavigationTimeoutError classifier: positive + negative cases.
  • bunx vitest run packages/engine/src/services/pageNavigationTimeoutErrorHint.test.ts15/15 passing.

  • Sibling regression check: bunx vitest run packages/engine/src/services/protocolTimeoutErrorHint.test.ts10/10 still passing (the two augmenters are mutually exclusive, so composing them at the catch site can't regress the sibling).

  • bun run typecheck in packages/engine — clean.

  • bun run typecheck in packages/producer — clean.

  • bunx oxfmt --write applied to all touched files.

  • bunx oxlint on new + touched sources — 0 warnings, 0 errors.

  • Unit tests added/updated

  • Manual testing performed — reproduction requires an actual darwin/arm64 CSS-3D + audio composition that trips the 60s page.goto timeout; the mocked tests cover both the generic and compound augmentation paths and the fallback for unknown-flag inputs.

  • Documentation updated — docs already list PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS on the --browser-timeout row (docs/packages/cli.mdx:720). No new doc entry needed; the error-message text is the doc.

Enterprise release / feature flag holdout

  • No enterprise release / feature flag holdout required — error-message text change, no runtime behaviour change (matcher is a no-op on non-matching errors).
  • Enterprise release / feature flag holdout required — coordinated with enterprise team.

UX/Screenshot recording

  • No UI impact — CLI error-message text only.

Stack: PR #3 of 9. Base: via/win32-streaming-encode-autodisable (#2505).

— Via

Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com

Field signal ts=1784146416 (darwin/arm64, CLI 0.7.58, 7/10): host
page.goto hit Navigation timeout of 60000ms twice on a CSS 3D + audio
composition; Docker rendered the same composition successfully.
Puppeteer's stock "Navigation timeout of 60000 ms exceeded" text names
none of HyperFrames' existing escape hatches, so the reporter had no
signal that the failure had knobs.

Wraps main-render Puppeteer `page.goto` errors matching
/Navigation timeout|net::ERR_TIMED_OUT/i with an augmented message that
names:

- The effective timeout currently applied (`cfg.pageNavigationTimeout`).
- Raise-the-timeout: `PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS` env,
  `--browser-timeout` CLI flag (seconds).
- Browser-binary escape hatch: `HYPERFRAMES_BROWSER_PATH` env.
- Field-signal shape: darwin/arm64 + CSS 3D + audio compound Docker
  hint — gated on all three inputs being explicitly true; falls back
  to generic hints when any input is unknown.

Mirrors #2443's HYPERFRAMES_BROWSER_PATH surfacing pattern (which
covered download-time failures) at the runtime `page.goto` layer.
Non-matching errors flow through unchanged. Original error preserved
via `err.cause`.

Wired into `renderOrchestrator.executeRenderJob`'s top-level catch,
composed after `augmentProtocolTimeoutError` so the two augmenters
never both fire on the same error (mutually exclusive regexes).
Current wire-up passes no `hasCss3D` / `hasAudio` context — no
compile-time CSS-3D signal is threaded through the render pipeline,
and `hasAudio` is block-scoped inside the try. Per the helper's
fallback docs, unknown flags route to the generic env + browser-path
hints. A future compile-time CSS-3D scan can thread both flags to
enable the full compound Docker hint without touching this helper's
signature.

Stack: PR #3 of 9 (base via/win32-streaming-encode-autodisable).

Signed-off-by: Via <vance@heygen.com>

@miga-heygen miga-heygen 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.

Review — #2506 feat(engine): surface escape hatches in page.goto Nav timeout errors

Verdict: LGTM — clean sibling to #2504, well-documented fallback design.

SSOT check

Item Owner Duplicates?
Nav-timeout detection NAVIGATION_TIMEOUT_MATCHER regex (shared by augment + predicate) No overlap with #2504's PROTOCOL_TIMEOUT_MATCHER — mutually exclusive classes
Env var name PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS Matches existing config.ts usage
CLI flag --browser-timeout (seconds) Already documented in cli.mdx
Browser escape hatch HYPERFRAMES_BROWSER_PATH Same surface as #2443 and #2481
Docker compound hint shouldSurfaceDockerHint (darwin + arm64 + CSS 3D + audio) Single gate function, strict === true checks

Design review

The compound Docker hint is correctly conservative:

  • Gated on ALL FOUR flags being explicitly true (=== true, not truthy)
  • undefined inputs (current production state — hasCss3D not threaded, hasAudio block-scoped in try) correctly suppress the Docker hint
  • Generic env + browser-path hints still fire — 3 escape hatches the reporter didn't have before
  • Future-proofed: the NavigationTimeoutHintContext interface accepts both flags so a compile-time hasCss3D scan can enable the full compound hint without touching the helper

The composition in renderOrchestrator.ts is correct:

error → augmentProtocolTimeoutError → augmentPageNavigationTimeoutError → normalizeErrorMessage

Mutually exclusive matchers mean at most one augmentation fires. Non-matching errors pass through as the same instance at each step.

Tests

15 cases — thorough coverage:

  • Pass-through for non-Nav errors, augmentation with effective timeout, env+CLI+browser-path hints, cause preservation, net::ERR_TIMED_OUT variant, non-Error coercion
  • Docker hint: all-four-true fires, non-darwin suppressed, darwin/x64 suppressed, missing CSS 3D suppressed, missing audio suppressed, undefined inputs suppressed
  • Platform/arch defaults when context omitted
  • Predicate positive/negative branches

No blockers. Ship it.

Base automatically changed from via/win32-streaming-encode-autodisable to main July 16, 2026 00:21
@vanceingalls vanceingalls merged commit 9db5f67 into main Jul 16, 2026
26 checks passed
@vanceingalls vanceingalls deleted the via/darwin-goto-nav-timeout-hint branch July 16, 2026 00:22
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