Skip to content

Clipboard: don't fail when the clipboard changes during a read - #219

Open
emrul wants to merge 1 commit into
kasmtech:masterfrom
emrul:bugfix/clipboard-changed-mid-read
Open

emrul wants to merge 1 commit into
kasmtech:masterfrom
emrul:bugfix/clipboard-changed-mid-read

Conversation

@emrul

@emrul emrul commented Oct 10, 2026

Copy link
Copy Markdown

Every so often, clicking into a session shows the noVNC error overlay:

InvalidStateError: Failed to execute 'getType' on 'ClipboardItem': Clipboard data has changed
    at clipboardPasteDataFrom (core/rfb.js)

It happens on stock workspaces too.

Cause. With binary clipboard on, the first user-driven event after the tab regains focus calls checkLocalClipboard(), which runs navigator.clipboard.read() and passes the items to clipboardPasteDataFrom(). In Chrome a ClipboardItem doesn't hold the data: getType() reads the system clipboard when it is called, and throws InvalidStateError if the clipboard changed after read(). The loop awaits getType() and arrayBuffer() once per type, so anything that copies in between makes it throw:

  • selecting text in the session. The click starts the read, and the server sends the selection straight back, so _write_binary_clipboard() writes the clipboard mid-read
  • a clipboard manager on the client
  • a copy on the client just as the tab is focused

The rejection from the async clipboardPasteDataFrom() isn't caught (the .then(ok, err) only covers read()), so app/error-handler.js shows it. There are two side effects:

  • the local clipboard isn't sent, because _resendClipboardNextUserDrivenEvent was already cleared
  • if the first type was read before a later one threw, _clipHash keeps its hash, so copying the same thing again is skipped as "No clipboard changes"

Fix (core/rfb.js):

  • _sendLocalClipboard() waits for an in-flight _write_binary_clipboard() before reading. If InvalidStateError happens anyway, it calls read() once more. If that also fails, it sets _resendClipboardNextUserDrivenEvent again so the next click retries, and the rejection is caught and logged at debug level.
  • _clipHash is set only once every type has been read and the clipboard is sent.
  • _write_binary_clipboard() keeps its promise in _clipboardWrite. Its writeText fallback is now returned, so that promise covers the fallback too.

Tested by loading core/rfb.js in Node with a mocked navigator.clipboard whose getType() throws like Chrome's once the clipboard has changed:

  • Before the fix: the same unhandled InvalidStateError, nothing sent, and the resend flag left cleared.
  • After the fix:
    • one change mid-read → the second read sends the new clipboard
    • a change on every read → nothing thrown, the flag is set again, and the hash is unchanged
    • a server write in flight → the read waits for it

Linting core/rfb.js gives the same results before and after.

A ClipboardItem from navigator.clipboard.read() reads the system
clipboard when getType() is called, and Chrome throws "InvalidStateError:
Clipboard data has changed" if it changed after read(). Clicking into the
session after focusing the tab reads the local clipboard; anything that
copies in that window (selecting text in the session, which the server
sends straight back as a clipboard write, a clipboard manager, a copy on
the client) made getType() throw. Nothing caught it, so it reached the
noVNC error overlay, the local clipboard wasn't sent, and if the first
type had been read its hash was kept, so the same copy was skipped as
"No clipboard changes" afterwards.

Wait for our own clipboard write to finish before reading, read once
more if the clipboard changes anyway, and failing that try again on the
next user-driven event. Record the hash only once the clipboard is sent.
@gnawhleinad

Copy link
Copy Markdown

I've applied this fix and confirmed that it resolves Chrome's InvalidStateError error.

@gnawhleinad

Copy link
Copy Markdown

I've tried applying this fix on top of v1.5.2. Unfortunately, I'm getting the clipboard failure and noVNC encountered an error. crash... 😢

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.

3 participants