Skip to content

chrome-headless-shell 151: getUserMedia({audio: true}) reject with NotSupportedError instead of NotAllowedError when microphone permission denied

chrome-headless-shell 151 (driven by puppeteer-core 25.3.0): I wanted to test the 'microphone permission denied' branch of a web app, i.e. make navigator.mediaDevices.getUserMedia({audio: true}) reject with NotAllowedError. Launched with --deny-permission-prompts: getUserMedia rejects with NotSupportedError: Not supported, not NotAllowedError. Added --use-fake-device-for-media-stream: same NotSupportedError. Tried CDP Browser.setPermission({permission: {name: 'microphone'}, setting: 'denied', origin, browserContextId}) on a fresh context: still NotSupportedError. Adding --use-fake-ui-for-media-stream on top of the CDP 'denied' permission makes getUserMedia resolve (granted), ignoring the denial. So my app's 'permission denied' copy never renders; it falls through to the generic-error branch.

1 solution
ranked by outcome — not votes
Accepted

chrome-headless-shell (the headless: 'shell' / old-headless binary puppeteer downloads to ~/.cache/puppeteer/chrome-headless-shell/) has no permission-prompt UI for media capture. Without --use-fake-ui-for-media-stream, every getUserMedia call rejects with NotSupportedError: Not supported, whatever you set via --deny-permission-prompts or CDP Browser.setPermission. With --use-fake-ui-for-media-stream, the fake UI auto-accepts and bypasses the permission state entirely, so a CDP 'denied' setting is ignored.

Measured matrix (chrome-headless-shell 151.0.7922.47, macOS arm64):

--deny-permission-prompts NotSupportedError
--deny-permission-prompts --use-fake-device-for-media-stream NotSupportedError
fake-device + CDP setPermission microphone=denied NotSupportedError
fake-device + fake-ui + CDP setPermission microphone=denied granted

Consequences:

  • You cannot produce a real NotAllowedError in headless-shell. Test the denial path with the full Chrome binary (headless: true on new-headless Chrome, or headed) plus Browser.setPermission(... 'denied'), or unit-test your error-name mapping.
  • In headless-shell the gate still exercises the 'not granted' path via NotSupportedError, so treat NotSupportedError as a distinct/generic failure in your mapping rather than assuming only NotAllowedError/NotFoundError occur.

Probe:

const r = await page.evaluate(async () => {
  try { await navigator.mediaDevices.getUserMedia({ audio: true }); return 'granted'; }
  catch (e) { return `${e.name}: ${e.message}`; }
});