Skip to content

fix(web): recover file-upload state when a paste is interrupted or never pulled#1372

Open
Yuval Marcus (ymarcus93) wants to merge 1 commit into
Devolutions:masterfrom
ymarcus93:yuval/upload-wedge-fix
Open

fix(web): recover file-upload state when a paste is interrupted or never pulled#1372
Yuval Marcus (ymarcus93) wants to merge 1 commit into
Devolutions:masterfrom
ymarcus93:yuval/upload-wedge-fix

Conversation

@ymarcus93

@ymarcus93 Yuval Marcus (ymarcus93) commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Problem

RdpFileTransferProvider.uploadFiles() sets uploadState and refuses to start another upload while
it is set (throw "Upload already in progress"). uploadState was cleared only when the remote
pulled the advertised files to completion, or on dispose(). So any paste that is advertised but
doesn't run to completion wedges every later upload until the page is reloaded. That happens whenever:

  • The remote never pulls the files — the paste lands in a non-file target, the clipboard monitor
    overwrites our file FormatList before the paste is pulled, or the remote rejects the FormatList with
    CB_RESPONSE_FAIL so it never requests the files.
  • The remote replaces the clipboard mid-paste — it copies its own content, which per MS-RDPECLIP
    makes it the new clipboard owner and supersedes our advertise, so our files are never pulled.
  • The remote starts pulling, then stops — the transfer stalls partway, so completion (the only
    thing that cleared uploadState) never arrives.

Backends also had no way to know whether an advertise was accepted or rejected.

Fix

Release uploadState (reject completion, emit an upload error, resume clipboard monitoring) on
every path where an advertised paste won't run to completion — not just the happy-path completion:

  1. Paste-ack watchdog (primary, peer-agnostic). Armed on advertise, disarmed on the first
    FileContentsRequest. On timeout (60s, matching Cliprdr's lock_inactivity_timeout) it fails the
    pending upload regardless of how the peer behaves. Covers "advertised but never pulled."
  2. on_format_list_response (explicit reject). Newly surfaced from the cliprdr hook (feat(cliprdr): add CliprdrBackend::on_format_list_response(ok) hook #1300)
    through ironrdp-web as a formatListResponseCallback / format-list-response event; on
    CB_RESPONSE_FAIL the provider fails the pending upload at once. The event also lets a frontend
    drive paste (inject on accept, retry on reject).
  3. handleFilesAvailable (remote replaced the clipboard with files). When the remote copies its
    own files mid-paste the web client auto-requests the list (no delayed rendering) and surfaces it
    here — i.e. the remote took clipboard ownership, so our advertise is superseded and we fail the
    pending upload. The symmetric counterpart of (2): that recovers when the remote rejects our
    advertise, this when it replaces it.
  4. Upload inactivity watchdog. Armed and reset on every FileContentsRequest once pulling has
    begun — picking up where the paste-ack watchdog (which only covers the never-pulled case) leaves
    off. If requests then stop for 60s the paste stalled — e.g. a text/image copy took the clipboard
    (which arrives as a FormatList without FileGroupDescriptorW, so it never reaches
    handleFilesAvailable), or the transfer simply died — and the upload is failed. A
    slow-but-progressing transfer keeps resetting it, so it is never killed.

Together (1) and (4) give continuous coverage across the paste's lifetime (before the first pull, then
after it), while (2) and (3) short-circuit the two cases the remote signals explicitly.

Monitoring stays suppressed until the first FileContentsRequest (not just the wire send), closing the
window where the clipboard monitor could clobber our file FormatList.

handleUnlock is a deliberate no-op: Unlock is not a reliable failure signal — the peer, and our own cliprdr on the lock-inactivity timeout, emit it routinely (often before any FileContentsRequest) — so recovery is
left to the watchdogs.

Scope

  • ironrdp-web: FormatListResponse backend message + on_format_list_response wiring; the cliprdr
    trait method has a no-op default (feat(cliprdr): add CliprdrBackend::on_format_list_response(ok) hook #1300), so it is non-breaking for other backends.
  • iron-remote-desktop-rdp (web): provider recovery paths (paste-ack + inactivity watchdogs,
    handleFilesAvailable release, reject handling) and a formatListResponseCallback factory. No
    iron-remote-desktop change.

Tests

RdpFileTransferProvider.test.ts:

  • Monitoring is deferred past the wire send and resumed on the first FileContentsRequest.
  • The paste-ack watchdog fails a never-pulled upload and resumes monitoring (and the next upload then
    succeeds).
  • The inactivity watchdog fails a pulled-then-idle upload, releasing the wedge, but does not fail a
    pulled upload that keeps making progress.
  • handleFilesAvailable releases an in-flight upload when the remote replaces the clipboard.
  • An Unlock does not fail an in-flight upload (it is not a paste-failure signal).
  • on_format_list_response: extension exposure, event emission, reject-on-fail, and a reject with no
    upload in progress is a no-op.

@ymarcus93 Yuval Marcus (ymarcus93) force-pushed the yuval/upload-wedge-fix branch 2 times, most recently from 3c14ae1 to b3dc24f Compare June 10, 2026 21:59
@ymarcus93 Yuval Marcus (ymarcus93) marked this pull request as ready for review June 10, 2026 23:06
@ymarcus93

Copy link
Copy Markdown
Contributor Author

Hey Benoît Cortier (@CBenoit), do you mind taking a look at this PR? It fixes an issue where file upload no longer works (with no way to recover except a hard refresh) if we get to this broken state

@ymarcus93 Yuval Marcus (ymarcus93) changed the title fix(web): recover file-upload state on an un-pulled paste fix(web): recover file-upload state when a paste is interrupted or never pulled Jun 12, 2026

Copilot AI 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.

Pull request overview

This PR hardens the web RDP file-upload (“clipboard file paste”) flow so that an advertised upload can’t wedge future uploads when the remote never pulls, rejects, supersedes, or stalls the paste. It also surfaces the remote FormatListResponse (accept/reject) signal from cliprdr through ironrdp-web into iron-remote-desktop-rdp, and adds unit tests for the new recovery paths.

Changes:

  • Add paste-ack + inactivity watchdogs and centralized “fail pending upload” cleanup to reliably clear uploadState and resume clipboard monitoring.
  • Surface on_format_list_response as a JS callback / format-list-response event and use it to fail early on explicit reject.
  • Expand RdpFileTransferProvider tests to cover deferred monitoring resume, watchdog timeouts, remote clipboard replacement, and format-list response behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
web-client/iron-remote-desktop-rdp/src/RdpFileTransferProvider.ts Adds watchdogs + new recovery paths, monitoring suppression deferral, and format-list-response event wiring.
web-client/iron-remote-desktop-rdp/src/RdpFileTransferProvider.test.ts Adds regression tests for the new watchdog behavior and format-list response handling.
web-client/iron-remote-desktop-rdp/src/extensions.ts Adds a builder extension factory for format_list_response_callback.
crates/ironrdp-web/src/session.rs Threads the new callback through SessionBuilder into the WASM clipboard backend callbacks.
crates/ironrdp-web/src/clipboard.rs Adds a backend message + JS callback dispatch for FormatListResponse and implements the on_format_list_response hook.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +303 to +308
// Upload paste-window watchdog. Armed when we advertise an upload, disarmed on the
// first FileContentsRequest (the remote pulled the files). Being armed is the single
// "advertised but not yet pulled" signal: on timeout it resumes monitoring and fails
// the never-pulled upload, and handleUnlock / handleFormatListResponse consult it to
// fail a refused paste early. Upload completion/failure run only after that first
// request, so they need not touch it.
Comment on lines +724 to +728
/** Suppress clipboard monitoring for the duration of an upload's paste window. */
private suppressUploadMonitoring(): void {
this.onUploadStarted?.();
this.uploadMonitoringSuppressed = true;
}
Comment on lines +732 to +738
private resumeUploadMonitoring(): void {
if (!this.uploadMonitoringSuppressed) {
return;
}
this.uploadMonitoringSuppressed = false;
this.onUploadFinished?.();
}
Comment on lines +861 to +863
} else {
warn!(ok, "Format list response received but no JS callback registered");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants