Skip to content

fix(huddle): guard mediaDevices access when the API is unavailable - #3817

Open
dpetrakov wants to merge 3 commits into
block:mainfrom
dpetrakov:fix/huddle-mediadevices-guard
Open

fix(huddle): guard mediaDevices access when the API is unavailable#3817
dpetrakov wants to merge 3 commits into
block:mainfrom
dpetrakov:fix/huddle-mediadevices-guard

Conversation

@dpetrakov

@dpetrakov dpetrakov commented Jul 30, 2026

Copy link
Copy Markdown

Summary

navigator.mediaDevices is undefined in a non-secure context, for example a WKWebView that never got a secure origin. Huddle touched it unguarded from two mount-time effects, so the whole React tree crashed to the "Something went wrong" boundary with undefined is not an object (evaluating 'navigator.mediaDevices.enumerateDevices'). That happened before the user ever started a huddle.

All five call sites now go through lib/mediaDevices.ts, which mirrors the guard this codebase already uses at desktop/src/features/profile/lib/animatedAvatarCapture.ts:131.

Site Before After
lib/useAudioDevices.ts, enumerateDevices + devicechange crash on mount input device list stays empty, listener skipped
HuddleContext.tsx, output-device devicechange crash on mount listener skipped. The list itself is unaffected, it comes from Rust via list_audio_output_devices
HuddleContext.tsx, getUserMedia on join opaque crash throws a sentinel that formatHuddleActionError maps to "Microphone access isn't available in this window. Try restarting Buzz."

Two shapes worth explaining. availableMediaDevices() returns MediaDevices | null for the paths that can degrade, and requireMediaDevices() throws the sentinel for the join path, which cannot. And the missing-API warning is latched to fire once per process: three call sites reach it and mount effects run twice under React.StrictMode, so an unguarded log would print five identical lines and bury the signal. The latch is environment state rather than community state, so it is deliberately not registered in resetCommunityState().

Related issue

Fixes #3118.

Duplicate search on 2026-08-09: no open PR touches huddle plus mediaDevices/getUserMedia. Closest existing PR: none found.

Testing

just ci passes end to end on macOS arm64, rebased on 5bf78671f. Desktop suite is 4545 tests, 0 failures.

The regression test is the one that matters. useAudioDevices.test.mjs renders a harness around the hook under jsdom, which implements no navigator.mediaDevices and is therefore exactly the state this issue describes. I checked that it can actually fail: reverting the guard makes it throw TypeError: Cannot read properties of undefined (reading 'enumerateDevices'), the same error in the report.

mediaDevices.test.mjs covers the helper directly, including each way the API can be missing and the once-per-process latch. That last case lives in mediaDevicesWarning.test.mjs because the node runner gives every test file its own process, and it is the only place the latch is observable unset.

huddleError.test.mjs covers the new copy. The pre-existing case that passes the literal string "Microphone unavailable" still returns it verbatim, because the sentinel matches on microphone_unavailable with an underscore.

No screenshots: this replaces a crash with a degradation path. The only new visible string is the error copy quoted above.

Rebase note

Rebased onto current main after the Huddle redesign (#4281). The conflict was an import collision, nothing structural, and all guard sites reapplied cleanly.

One thing the redesign forced: HuddleContext.tsx came out of it at exactly the 1000-line ratchet limit, and this change pushed it to 1007. Rather than touch the limit, I moved the throw-or-return logic into requireMediaDevices(). The file is now 997 lines, two below where main has it.

Follow-up, deliberately not here

profile/lib/animatedAvatarCapture.ts:157 has the same unguarded getUserMedia, noted in the issue. Different feature, so it is a separate change. Say the word and I will send it.

@dpetrakov
dpetrakov requested a review from a team as a code owner July 30, 2026 22:24
@Chessing234

Copy link
Copy Markdown
Contributor

sensible guard. a one-line log when mediaDevices is missing would help support spot locked-down environments

dpetrakov added a commit to dpetrakov/buzz that referenced this pull request Jul 31, 2026
Review feedback on block#3817: the guard left the mount-time path silent, so a
locked-down environment produced an empty device list and no diagnostic at
all. Support only had a signal once someone tried to join, and even then
only the user-facing copy.

Emit one `console.warn` from `availableMediaDevices()`, latched so it fires
at most once per process. Three call sites reach it and mount-time effects
run twice under `React.StrictMode`, so an unguarded log would emit five
identical lines and bury the signal.

The latch is environment state, not community state, so it is deliberately
not wired into `resetCommunityState()`.

Signed-off-by: Dmitry Petrakov <dpetrakov@hotmail.com>
@dpetrakov

Copy link
Copy Markdown
Author

Good call – the mount path was silent, so support had nothing to go on until someone actually tried to join, and even then only the user-facing copy. Added in 47518fb.

One deliberate wrinkle: the warning is latched to fire at most once per process rather than once per call. availableMediaDevices() is reached from three sites and main.tsx runs under React.StrictMode, so mount effects execute twice – an unguarded log would emit five identical lines and bury the signal it exists to provide.

Two notes for whoever reviews it:

  • The latch is module-level state, which usually has to be registered in resetCommunityState(). Not here: whether the webview exposes mediaDevices is a property of the environment, not of the community, and cannot change on a community switch. Called out in a comment at the declaration so it does not read as an oversight.
  • The once-per-process test lives in its own file, mediaDevicesWarning.test.mjs. The node runner gives each test file its own process, so that is the only place the latch is observable in its initial state; folding those cases into mediaDevices.test.mjs would make them depend on declaration order, since the tests there consume the latch on their first null result.

just ci is green end to end on macOS arm64.

`navigator.mediaDevices` is undefined in a non-secure context, and huddle
touched it unguarded from two mount-time effects. The whole React tree
crashed to the error boundary with "undefined is not an object (evaluating
'navigator.mediaDevices.enumerateDevices')" before the user ever started a
huddle.

Route the five call sites through `availableMediaDevices()`, mirroring the
guard already used in `profile/lib/animatedAvatarCapture.ts`. The input
device list degrades to empty and the hot-plug listeners are skipped; the
output device list is unaffected because it comes from Rust. The
`getUserMedia` join path now throws a recognizable sentinel that
`formatHuddleActionError` maps to actionable copy instead of crashing.

Fixes block#3118

Signed-off-by: Dmitry Petrakov <dpetrakov@hotmail.com>
Review feedback on block#3817: the guard left the mount-time path silent, so a
locked-down environment produced an empty device list and no diagnostic at
all. Support only had a signal once someone tried to join, and even then
only the user-facing copy.

Emit one `console.warn` from `availableMediaDevices()`, latched so it fires
at most once per process. Three call sites reach it and mount-time effects
run twice under `React.StrictMode`, so an unguarded log would emit five
identical lines and bury the signal.

The latch is environment state, not community state, so it is deliberately
not wired into `resetCommunityState()`.

Signed-off-by: Dmitry Petrakov <dpetrakov@hotmail.com>
The guards themselves had no coverage: the helper was unit-tested, but
nothing exercised the actual regression, which is a component throwing
during its mount effect. jsdom and @testing-library/react landed in the
desktop dev deps since this branch was opened, so that gap can now be
closed.

jsdom implements no `navigator.mediaDevices`, which is precisely the
non-secure-context state from block#3118. Rendering a harness around
`useAudioDevices` in that environment reproduces the crash: reverting the
guard fails this test with `TypeError: Cannot read properties of undefined
(reading 'enumerateDevices')`, the same error the issue reports.

Signed-off-by: Dmitry Petrakov <dpetrakov@hotmail.com>
@dpetrakov
dpetrakov force-pushed the fix/huddle-mediadevices-guard branch from 47518fb to 8130da5 Compare August 9, 2026 09:35
@dpetrakov

Copy link
Copy Markdown
Author

Rebased onto 5bf78671f and updated the description. This had gone stale after the Huddle redesign (#4281), so a few notes on what changed.

The bug is still there on current main. I checked before touching anything: all six navigator.mediaDevices call sites in the feature are still unguarded, just at different line numbers. HuddleContext.tsx moved 168 to 219 and 374 to 584.

The conflict itself was an import collision, nothing structural, and every guard reapplied cleanly.

Two things worth calling out.

The redesign left HuddleContext.tsx sitting at exactly the 1000-line ratchet limit, and my +7 broke it. Instead of touching the limit I moved the throw-or-return logic into requireMediaDevices() in the helper. That reads better anyway: "try and return null" and "require or throw" now live next to each other rather than one of them being inlined in the context. The file is 997 lines now, two below where main has it.

And jsdom plus @testing-library/react landed in the desktop dev deps while this sat. When I opened the PR I wrote that the guards themselves could not be tested without a DOM harness. That is no longer true, so I closed the gap: useAudioDevices.test.mjs renders the hook under jsdom, which implements no navigator.mediaDevices and is therefore the exact state this issue describes. I checked the test can fail. Reverting the guard makes it throw TypeError: Cannot read properties of undefined (reading 'enumerateDevices'), which is the error in the original report.

just ci is green end to end on macOS arm64. Desktop suite 4545 tests, 0 failures.

Sorry about the force-push. Nothing had been reviewed yet, so no comment threads were harmed, and a merge commit seemed worse given the repo squash-merges.

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.

Huddle mediaDevices access crashes the whole app when navigator.mediaDevices is undefined

2 participants