Repository navigation
fix(web): guard MeshGradient against missing WebGL context - #6318
Conversation
📝 WalkthroughWalkthrough
ChangesMesh gradient lifecycle
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@packages/ui/src/components/mesh-gradient.tsx`:
- Around line 80-86: Update the effect around the delayed noiseSpeed update to
store the setTimeout handle before defining teardown, then clear that handle
inside teardown before disconnecting or releasing the gradient. Preserve the
existing callback behavior while ensuring it cannot run after cleanup or a
speed-triggered effect rerun.
🪄 Autofix
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 Plus
Run ID: 7f301c68-284d-46d1-ab45-170588b5e42a
📒 Files selected for processing (1)
packages/ui/src/components/mesh-gradient.tsx
| setTimeout(() => { | ||
| if (gradient?.uniforms?.u_global?.value?.noiseSpeed) { | ||
| gradient.uniforms.u_global.value.noiseSpeed.value = speed; | ||
| } | ||
| }, 100); | ||
|
|
||
| return teardown; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Cancel the delayed noiseSpeed update during teardown.
teardown does not retain the timeout handle. If the effect re-runs because speed changes, or the component unmounts before 100 ms, the callback still runs against the disconnected gradient. This creates stale work and can surface an uncaught post-cleanup error.
Store the handle before defining teardown, and clear it inside teardown.
Proposed fix
const gradient = new Gradient() as GradientInstance;
+ let noiseSpeedTimeout: ReturnType<typeof setTimeout> | undefined;
const teardown = () => {
+ if (noiseSpeedTimeout !== undefined) {
+ clearTimeout(noiseSpeedTimeout);
+ }
if (gradient.pause) {
gradient.pause();
}
// existing cleanup
};
- setTimeout(() => {
+ noiseSpeedTimeout = setTimeout(() => {
if (gradient?.uniforms?.u_global?.value?.noiseSpeed) {
gradient.uniforms.u_global.value.noiseSpeed.value = speed;
}
}, 100);🤖 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 `@packages/ui/src/components/mesh-gradient.tsx` around lines 80 - 86, Update
the effect around the delayed noiseSpeed update to store the setTimeout handle
before defining teardown, then clear that handle inside teardown before
disconnecting or releasing the gradient. Preserve the existing callback behavior
while ensuring it cannot run after cleanup or a speed-triggered effect rerun.
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
There was a problem hiding this comment.
2 issues found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/ui/src/components/mesh-gradient.tsx">
<violation number="1" location="packages/ui/src/components/mesh-gradient.tsx:29">
P2: Repeated mounts or `speed` updates leak probe contexts until browsers refuse the real gradient context, leaving otherwise-supported gradients on the static fallback. Cache the capability result (or explicitly lose the probe context) so the check does not allocate a new context per effect run.</violation>
<violation number="2" location="packages/ui/src/components/mesh-gradient.tsx:34">
P3: The `supportsWebgl()` probe swallows any error from `document.createElement` or `getContext` in a silent empty catch with no rationale comment and no observability. If the failure is something other than WebGL being genuinely unavailable (e.g. an allocation/sandboxing error), it becomes indistinguishable from real lack of support and goes completely unlogged. Since the rest of this PR adds an inline rationale comment to the init catch for exactly this reason, consider adding a brief inline comment here explaining that the probe is a best-effort feature check (so an unsupported environment simply keeps the static background), keeping the fix consistent with the swallow-with-rationale convention used below.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Browser as Browser
participant Canvas as <canvas> Element
participant Probe as WebGL Probe (throwaway)
participant Mesh as MeshGradient Component
participant Gradient as stripe-gradient Library
participant DOM as DOM (body)
Note over Mesh: Mount effect triggered
Mesh->>Canvas: canvasRef.current
alt Canvas is null (edge case)
Mesh-->>DOM: bail out early, no gradient
else Canvas exists
Mesh->>Probe: supportsWebgl() check
Probe->>Probe: document.createElement("canvas")
Probe->>Probe: probe.getContext("webgl2")
alt WebGL2 available
Probe-->>Mesh: true
else WebGL2 not available
Probe->>Probe: probe.getContext("webgl")
alt WebGL available
Probe-->>Mesh: true
else WebGL not available (blocklisted/headless/software)
Probe-->>Mesh: false
end
end
alt WebGL not supported
Mesh-->>DOM: bail out, leave static background
else WebGL supported
Mesh->>Gradient: new Gradient()
Gradient-->>Mesh: gradient instance
Mesh->>Gradient: gradient.initGradient(`#${canvasId}`)
alt Init succeeds
Gradient-->>Mesh: gradient initialized
Mesh->>Mesh: setTimeout(100ms)
Mesh->>Gradient: set noiseSpeed from props
Note over Mesh: Normal runtime — gradient animates
Note right of Mesh: teardown callback registered<br/>(pause + disconnect)
else Init fails (null context despite probe)
Note over Mesh,Gradient: NEW: try/catch guards init only
Mesh->>DOM: teardown() — pause + disconnect gradient
Mesh-->>DOM: return early, no crash
end
end
end
Note over Mesh: Cleanup on unmount or deps change
Mesh->>DOM: teardown() — pause + disconnect gradient
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| function supportsWebgl(): boolean { | ||
| try { | ||
| const probe = document.createElement("canvas"); | ||
| return Boolean(probe.getContext("webgl2") || probe.getContext("webgl")); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
P2: Repeated mounts or speed updates leak probe contexts until browsers refuse the real gradient context, leaving otherwise-supported gradients on the static fallback. Cache the capability result (or explicitly lose the probe context) so the check does not allocate a new context per effect run.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ui/src/components/mesh-gradient.tsx, line 29:
<comment>Repeated mounts or `speed` updates leak probe contexts until browsers refuse the real gradient context, leaving otherwise-supported gradients on the static fallback. Cache the capability result (or explicitly lose the probe context) so the check does not allocate a new context per effect run.</comment>
<file context>
@@ -26,6 +26,15 @@ interface GradientInstance {
};
}
+function supportsWebgl(): boolean {
+ try {
+ const probe = document.createElement("canvas");
</file context>
| function supportsWebgl(): boolean { | |
| try { | |
| const probe = document.createElement("canvas"); | |
| return Boolean(probe.getContext("webgl2") || probe.getContext("webgl")); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| let webglSupport: boolean | undefined; | |
| function supportsWebgl(): boolean { | |
| if (webglSupport !== undefined) return webglSupport; | |
| try { | |
| const probe = document.createElement("canvas"); | |
| webglSupport = Boolean( | |
| probe.getContext("webgl2") || probe.getContext("webgl"), | |
| ); | |
| } catch { | |
| webglSupport = false; | |
| } | |
| return webglSupport; | |
| } |
| const probe = document.createElement("canvas"); | ||
| return Boolean(probe.getContext("webgl2") || probe.getContext("webgl")); | ||
| } catch { | ||
| return false; |
There was a problem hiding this comment.
P3: The supportsWebgl() probe swallows any error from document.createElement or getContext in a silent empty catch with no rationale comment and no observability. If the failure is something other than WebGL being genuinely unavailable (e.g. an allocation/sandboxing error), it becomes indistinguishable from real lack of support and goes completely unlogged. Since the rest of this PR adds an inline rationale comment to the init catch for exactly this reason, consider adding a brief inline comment here explaining that the probe is a best-effort feature check (so an unsupported environment simply keeps the static background), keeping the fix consistent with the swallow-with-rationale convention used below.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ui/src/components/mesh-gradient.tsx, line 34:
<comment>The `supportsWebgl()` probe swallows any error from `document.createElement` or `getContext` in a silent empty catch with no rationale comment and no observability. If the failure is something other than WebGL being genuinely unavailable (e.g. an allocation/sandboxing error), it becomes indistinguishable from real lack of support and goes completely unlogged. Since the rest of this PR adds an inline rationale comment to the init catch for exactly this reason, consider adding a brief inline comment here explaining that the probe is a best-effort feature check (so an unsupported environment simply keeps the static background), keeping the fix consistent with the swallow-with-rationale convention used below.</comment>
<file context>
@@ -26,6 +26,15 @@ interface GradientInstance {
+ const probe = document.createElement("canvas");
+ return Boolean(probe.getContext("webgl2") || probe.getContext("webgl"));
+ } catch {
+ return false;
+ }
+}
</file context>
…y fix wave (#6437) * fix(desktop,host-service): address unreviewed findings from the sentry fix wave Four defects that automated reviewers flagged on PRs #6191, #6258, #6316 and #6318 and that were merged without anyone acting on them. - host-service-coordinator: crash reports redacted the child's own auth secret but not the cloud credential the coordinator also injects into its env, so a logged Authorization header could reach Sentry. Replace the single replacement with a per-instance redaction list. - workspaces/create: getCurrentBranch() returns null for a detached HEAD and for any git failure alike, so a missing repo or a permission wall was reported to the user as a detached HEAD and stopped being visible in Sentry. Confirm HEAD is genuinely detached first. - mesh-gradient: the WebGL probe leaked a context per mount, which can exhaust the browser's context budget and pin the real gradient to the static fallback. Release the probe and cache the result. - workspace-creation: existsSync() is true for a regular file, so a repo path pointing at a file passed the directory guard. * refactor(desktop): read HEAD once via a discriminated result instead of a second git client
Problem
Unhandled TypeErrors from the shared
MeshGradientcomponent in two apps:Cannot read properties of null (reading 'uniformMatrix4fv')in the web app (/integrations, /agents) andArgument 1 ('program') ... must be an instance of WebGLProgramon the marketing homepage. Triggered on browsers that cannot provide a WebGL context (GPU blocklisted, headless, software rendering, or context-creation failure). The gradient is purely decorative; the page is otherwise fine.Root cause
packages/ui/src/components/mesh-gradient.tsxinitializesstripe-gradientunconditionally in its mount effect. Whencanvas.getContext("webgl"/"webgl2")returns null, the library proceeds with the null context and dereferences it, throwing from library internals.Fix
Fix at the failure site in the shared component (no Sentry-side suppression):
webgl2 || webgl) before constructing the gradient; bail out and leave the static background when unavailable.initGradientcall in a try/catch scoped to initialization only — on failure, run the existing teardown (pause + decoy-element disconnect) and bail. Errors after successful init are not swallowed.Behaviour for users with working WebGL is unchanged.
Verification
bunx biome check packages/ui/src/components/mesh-gradient.tsx:cd packages/ui && bun run typecheck:Refs WEB-1W
Refs MARKETING-3E
https://claude.ai/code/session_4d58b950-d21f-466c-ba7a-bb20bf855a26
Summary by cubic
Guarded the shared MeshGradient against missing WebGL to stop runtime errors and leave a static background when WebGL isn’t available. Prevents crashes on web app integrations/agents and the marketing homepage; addresses WEB-1W and MARKETING-3E.
initGradientin try/catch; on failure, teardown (pause + disconnect) and exit. Behavior is unchanged when WebGL works.Written for commit 7da800b. Summary will update on new commits.
Summary by CodeRabbit