Skip to content
8 min read

A remedy you neither own nor version

A denied camera permission is remembered, and no API reopens it. So the recovery instruction ships as a screenshot of the browser's own menu, chosen by sniffing the user agent — an unversioned picture of somebody else's interface, on the one path that decides whether an appointment happens.

Vyacheslav Pankratov· Fullstack Developer
Cover art for “A remedy you neither own nor version”
On this page

TL;DR: A camera permission denied recovery path is the one part of a video product that cannot live in the product. The browser remembers the denial, refuses without opening a dialog, and offers no API to ask again — correctly, because the permission belongs to the user. So the remedy ships as instructions about somebody else's interface, usually a bundled screenshot picked by sniffing the user agent. It is the only screen in the product whose correctness is decided by a release you do not control and do not get told about.

There is a file in a system we built whose whole job is this one error. It is a dialog, and a large part of it is a fork on which browser you are using, choosing between two .jpg files.

Both images are photographs of a browser's own permission menu, with the relevant control circled. They are the entire recovery path. If a user has previously refused the camera, this dialog is what stands between them and their appointment.

Why can't the application just ask again?

Because a remembered denial is a decision, and the platform treats it as one.

When a site has been denied, the request does not reach a prompt again. The specification's own algorithm reads the stored permission state first, removes every device whose state is denied, and — if that empties the candidate set — jumps straight to its failure step, which sits before the step that would request permission. It goes further and names the consequence for the browser's own interface: "In the case of a stored persistent permission to begin capture, it is important that it is easy to find the list of granted permissions and revoke permissions the user wishes to revoke." The platform documentation puts the other half plainly — browsers must ask at least the first time, and ongoing permission is something the user grants.

There is no API to reopen it, and this is not an oversight to be worked around — a permission a site could re-request at will would be worth nothing. The control sits on the browser's own surface on purpose, and every route back to it runs through UI the site does not own.

The shape it takes is a binary:

{errorType === ACCESS_DENIED
  ? isBrowserA()
      ? <img src={accessDeniedA} />   // a photograph of one browser's menu
      : <img src={accessDeniedB} />   // shown to every browser that is not that one
  : null}

So a product that wants to help has exactly one move available: explain, in its own words and its own pixels, how to operate an interface belonging to someone else.

What does that actually commit you to?

A dependency with none of the properties you would demand of any other dependency.

A library you depend on The browser UI your remedy describes
Version pinned yes no — and you do not choose when it moves
Change announced changelog, semver announced to browser users, not to your build — and it names no artefact of yours
Breakage detectable tests, CI no signal at all; the picture just becomes wrong
Rollback possible not available to you
Cost of being wrong a bug a user who cannot start their appointment

Nothing in your pipeline can fail when a browser moves that control, renames it, or redesigns the menu around it. No test in your pipeline can hold a screenshot of somebody else's interface to account — visual diffing works, and it works against surfaces you render. The image stays exactly as correct-looking as it was the day it was taken, and the only detector is a user who cannot follow it — who, by construction, is already stuck and already unhappy.

  1. Permission denied and rememberedno dialog will open again
  2. Show recovery instructionsa picture and a sentence
  • The browser named in the sniffgets the image that matches it
  • Every browser that is neithergets one of those two images, of a menu that is not theirs
Two branches, two browsers served, and everything else handed a picture of a menu it does not have. The trap is what happens next: each engine that generates support tickets earns another branch and another screenshot, so the maintenance surface grows in proportion to how many of your users you are already failing.

Why is the browser sniff the worse half?

The screenshot at least degrades honestly: it becomes a picture of something that used to be true. The fork that chooses it fails in a way nobody looks at.

The pattern in the code is a binary: if this is one named browser, show image A, otherwise show image B. It reads as covering two cases, and it does cover two — the one it tests for and the one it falls back to. Every browser that is neither gets a picture of a menu that is not theirs, and a user on a third engine is told to click something that does not exist anywhere on their screen.

That is worse than no instructions. No instructions leave a competent user to find the site-settings control in their own address bar. Wrong instructions actively send them looking for a control that is not there.

And the sniff is not a bug someone introduced carelessly. It is what you write when you have decided to describe a specific interface — the moment you commit to naming a menu item, you must know whose menu it is. The sniff is a symptom of the screenshot, not an independent mistake.

What is the honest version?

Start by separating what the product can assert from what it cannot.

  • What you know: the request was refused and it was remembered. That is yours, and you can say it precisely: "your browser is remembering an earlier decision to block the camera for this site."
  • What you can point at without owning: the browser's own settings surface, named generically — the site permissions or privacy section — rather than illustrated. Text ages differently from pictures: it becomes vague before it becomes wrong.
  • What only the user can do: change it. Say that plainly instead of implying the product can.

If you keep an illustrated version because it genuinely helps, treat it as the perishable good it is: date it in the interface, give it an owner, and put its re-check on a schedule — the same discipline any other claim with a shelf life gets. An undated screenshot is a claim with no expiry that will still be rendering confidently three redesigns later.

The one thing not to do is the thing this shape encourages: quietly expanding the fork. Each new browser that generates support tickets earns another branch and another image, and the maintenance surface grows in proportion to how many of your users you are failing.

Where does this leave the appointment?

That is the part this failure is judged on, and it is why a video consultation is a harder case than a consumer product.

A user who cannot recover a permission in a consumer app tries again later. A patient in a scheduled slot has a clinician waiting, a fixed window, and no second attempt that does not cost somebody a booking. The recovery path is not a nice-to-have on that screen; it is the screen, and it is the one part of the product whose correctness is set by a release nobody in your organisation reads.

Which is worth saying to whoever owns the roadmap: this is not a UI polish item. It is a single point of failure with an external owner, and the mitigation is not a better screenshot.

FAQ

Isn't this what a pre-call device check is for? A pre-call step moves when the user meets the problem, which is a real improvement and widely recommended — and this article deliberately does not close on it, because it is thoroughly published by people who also sell it. Note what it does not change: whatever the pre-call step says about recovery is still a description of an interface you do not own. Moving the screen earlier does not move the dependency.

Is this the same as the camera not being found? No. A remembered per-site denial is a browser-level decision with a browser-level remedy. An operating-system-level refusal surfaces as absent hardware and needs a different diagnosis entirely — which is its own article.

Can permissions.query() at least tell us the state so we can pre-empt it? Often, and it is worth using to decide whether to show the dialog. It does not help with the content of the dialog, which is the expensive part, and its coverage across engines has never been uniform enough to be the only branch.

Is there any version of this that does not rot? Text degrades more gracefully than pictures, and a generic pointer to "your browser's site permissions" ages into vagueness rather than into falsehood. There is no version that never needs re-checking, which is why the recommendation is an owner and a date rather than a better artefact.

The three things worth taking away

The recovery path is a dependency, and it is the only one you cannot pin. No version, no changelog, no notice, no rollback, and no test that can fail when it breaks.

A user-agent fork on a two-branch picture serves the two browsers it names and misleads every other. It is not a separate mistake — it is what committing to describe a specific interface forces you into.

Date it and own it, or stop illustrating it. The choice is between a perishable asset with a schedule and a durable sentence that is merely vague. What is not available is a screenshot that stays true on its own.

If the one screen deciding whether your appointments start is a picture nobody has looked at since it was taken, putting a date and an owner on it is the kind of thing we come in for.

Was this helpful?

// Build it

Running this in production?

Real-time video breaks in the places a demo never reaches — negotiation, weak networks, the transcoding bill. Tell us what breaks and where; we reply within a day with a concrete next step.