Skip to content

fix(viewer): guard 3D viewer against null WebGL context (Sentry MONOREPO-EDITOR-59)#455

Open
anton-pascal wants to merge 3 commits into
mainfrom
fix/sentry-EDITOR-59
Open

fix(viewer): guard 3D viewer against null WebGL context (Sentry MONOREPO-EDITOR-59)#455
anton-pascal wants to merge 3 commits into
mainfrom
fix/sentry-EDITOR-59

Conversation

@anton-pascal

@anton-pascal anton-pascal commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Sentry MONOREPO-EDITOR-59

TypeError: Cannot read properties of null (reading 'getSupportedExtensions')5282 events / 0 users.

The 0-users signature is the tell: this is headless bot / crawler traffic (plus GPU-blocklisted or hardware-acceleration-disabled browsers) hitting the public /viewer/:id route.

Root cause

Stack: three.js three.webgpu.jsBackend/Renderer init → WebGPU is unavailable so three.js transparently falls back to a WebGL backendcanvas.getContext('webgl2' | 'webgl') returns null → three calls gl.getSupportedExtensions() on the null context and throws.

The viewer already has a capability gate (canMountGpuViewer, added in #403) plus an UnsupportedGpuViewerFallback. But the gate mounted the canvas whenever 'gpu' in navigator was truthy:

if (!('gpu' in navigator) && !canCreateWebGLContext()) return false

That navigator.gpu flag being present does not guarantee WebGPU can produce a device. On headless/blocklisted browsers the adapter request fails, three falls back to WebGL, and if no real WebGL context is obtainable either, renderer.init() throws on the null context — the crash above. So the "optimistic" branch let exactly the crashing population through.

The fix

Require an actually obtainable WebGL context before mounting, unconditionally:

function canMountGpuViewer() {
  if (typeof window === 'undefined') return false
  if (!canCreateWebGLContext()) return false
  return true
}

Rationale: the WebGPURenderer fallback path always needs WebGL, so a creatable WebGL context is the true, verifiable precondition for mounting. This converts the flaky "gpu-in-navigator" optimism into a hard gate:

  • Headless bots / blocklisted browsers (no creatable context) now hit the existing UnsupportedGpuViewerFallback UI instead of crashing.
  • Real WebGPU-capable browsers always expose WebGL too, so no capable device is newly excluded.

Minimal, low-risk: one predicate changed, reusing the existing canCreateWebGLContext() helper and the existing fallback UI. No renderer/init refactor.

Scope

Do not merge without review.


Note

Medium Risk
Changes how and when the 3D canvas mounts and how WebGPU/WebGL backends are selected; mistakes could affect real browsers or leave headless traffic in a hung init state, though behavior is covered by new tests.

Overview
Adds renderer-capability (detectRendererCapability, initializeGpuRenderer) so the viewer no longer relies on a pre-mount navigator.gpu / WebGL probe gate. The R3F gl factory now runs through this helper: request a WebGPU device (with a 4s timeout), probe WebGL on a separate canvas, create WebGPURenderer with { device } or { forceWebGL: true }, and on WebGPU init failure dispose and retry on WebGL before surfacing unsupported.

Viewer drops canMountViewer / inline capability helpers; showGpuFallback is driven only by rendererInitFailed after init fails. UnsupportedGpuViewerFallback moves to its own module (copy tweak). The capability APIs are re-exported from @pascal-app/viewer, with Bun tests covering timeouts, WebGL fallback, probe isolation, and the null-context failure path tied to Sentry MONOREPO-EDITOR-59.

Reviewed by Cursor Bugbot for commit ff7c81f. Bugbot is set up for automated code reviews on this repo. Configure here.

@anton-pascal

Copy link
Copy Markdown
Contributor Author

Nightly Sentry triage (2026-07-11) — EDITOR-59 recurred with a new latest event (issue 7415398488) but this time from transaction /editor/:projectId (Edge 144 / Windows), not just the public /viewer/:id route. Same root cause: three.js WebGPURenderer falls back to WebGLBackend and gl.getSupportedExtensions() runs on a null GL context (WebGLBackend.initnew WebGLExtensions).

Heads up that the guard in this PR touches packages/viewer, but the editor route instantiates its own new WebGPURenderer(...) (e.g. the FloorPlanModal / editor canvas) that isn't covered here. Consider extending the null-context guard to the shared renderer-creation helper so both entry points are protected. No duplicate PR opened.

@anton-pascal

Copy link
Copy Markdown
Contributor Author

Nightly Sentry triage 2026-07-13: MONOREPO-EDITOR-59 (Cannot read properties of null (reading 'getSupportedExtensions')) is still firing in production (latest event 2026-07-13T00:11Z, release ef1eecc6, Chrome 150/Win). Stack confirms three.webgpu.js WebGLBackend.initnew WebGLExtensions on a null GL context — exactly what this PR guards. Bumping visibility for review/merge.

@anton-pascal

Copy link
Copy Markdown
Contributor Author

Sentry MONOREPO-EDITOR-FM (TypeError: this.gl is null, /viewer/:id, first seen 2026-07-15, 1 event / 0 users) is the same null-WebGL-context init class this PR addresses — three.js three.webgpu.jsWebGLBackend.initnew WebGLExtensions failing when the context is null (headless / GPU-blocklisted clients). No separate fix needed; this guard should cover FM too. Noting here to avoid a duplicate PR.

@anton-pascal

Copy link
Copy Markdown
Contributor Author

Nightly Sentry triage (2026-07-17): MONOREPO-EDITOR-FM (Cannot read properties of null (reading 'getSupportedExtensions'), last seen 07-17 on /viewer/:id) is the same crash signature this PR targets (EDITOR-59, three.webgpu.js WebGLBackend.init on a null GL context). This PR's hard canCreateWebGLContext() gate should cover FM too. Flagging so we don't open a duplicate — recommend review/merge, then verify FM stops firing.

@anton-pascal

Copy link
Copy Markdown
Contributor Author

Nightly Sentry triage (2026-07-18): MONOREPO-EDITOR-FM (Cannot read properties of null (reading 'getSupportedExtensions'), three.webgpu.js → WebGLBackend.init → new WebGLExtensions, route /viewer/:id) is the same null-WebGL-context class this PR guards against. The captured event was from a WebPageTest.org headless bot (no GPU), so it's largely crawler noise — but this PR's null-context guard should suppress it. Recommend also filtering WebPageTest/headless UAs in Sentry beforeSend. No duplicate PR opened.

@Aymericr

Copy link
Copy Markdown
Contributor

Triage result: the null-context diagnosis is credible, but this one-predicate patch is not complete enough to merge.

The follow-up Sentry events in this thread show the same crash on /editor/:projectId, while this branch only changes the Viewer mount gate. Also, requiring WebGL unconditionally assumes every usable WebGPU environment exposes WebGL; the robust contract is to centralize renderer capability/initialization and cover every canvas entry point, with the existing unsupported-GPU fallback handling init failure.

Please rebase onto current main and update this as a shared gate/fallback with regression coverage for: no navigator.gpu + no WebGL, navigator.gpu present but adapter/device unavailable, and renderer initialization failure. I’m keeping it open because the production signal is still active, but the current diff is blocked on that scope.

@anton-pascal
anton-pascal force-pushed the fix/sentry-EDITOR-59 branch from c80734d to 81b1661 Compare July 20, 2026 13:08
@anton-pascal

Copy link
Copy Markdown
Contributor Author

Reworked and rebased this onto current main (1d43a39f). The old one-predicate mount guard has been replaced with a shared renderer capability/initialization contract in @pascal-app/viewer:

  • All canvas entry points: the repo has a single R3F <Canvas> / WebGPURenderer construction site in the shared <Viewer>. The editor 3D canvas (/editor/:projectId), editor preview, public viewer, and IFC viewer all mount that component, so they now go through the same gate (including the editor/FloorPlanModal concern from the review).
  • WebGPU-only environments: capability detection requests a WebGPU adapter/device first and passes the working device into WebGPURenderer; WebGL is not required when WebGPU succeeds.
  • WebGPU unavailable: if the adapter/device cannot be obtained, the helper probes an actual WebGL2 context and explicitly initializes the WebGL backend with that context.
  • Initialization failures: renderer construction and init() are caught centrally, the partial renderer is disposed, and <Viewer> switches to the existing unsupported-GPU fallback rather than rejecting into R3F.
  • Regression coverage: added cases for no WebGPU + no WebGL, WebGPU present but adapter/device unavailable (with and without WebGL fallback), WebGPU without WebGL, and renderer init() throwing.

Fresh verification after the rebase:

  • bun test: 2,498 passed, 1 skipped, 0 failed
  • bun run check-types: passed
  • bun run lint: passed
  • bun run check: passed

@Aymericr re-requesting review on the centralized implementation.

@anton-pascal
anton-pascal requested a review from Aymericr July 20, 2026 13:08
Comment thread packages/viewer/src/components/viewer/index.tsx
Comment thread packages/viewer/src/lib/renderer-capability.ts
Comment thread packages/viewer/src/lib/renderer-capability.ts Outdated
Comment thread packages/viewer/src/lib/renderer-capability.ts

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ff7c81f. Configure here.

})

return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeout))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Timed-out GPU work left running

Medium Severity

withTimeout only races promises; it does not abort requestAdapter / requestDevice / init. A late-resolved GPUDevice is never destroyed, so timed-out capability checks can leave orphan devices that count against the browser’s tight concurrent-device limit.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ff7c81f. Configure here.

@anton-pascal

Copy link
Copy Markdown
Contributor Author

Nightly Sentry triage 2026-07-21: this error class is still occurring on latest release 4bcc2e2 as a new issue ID — MONOREPO-EDITOR-FM Cannot read properties of null (reading 'getSupportedExtensions') (Chrome 150/Windows, 7/21 02:04 UTC). Same root cause: WebGL2 context creation returns null after WebGPU fallback. Bumping this PR — merging it should also resolve/dedupe FM.

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