Skip to content

fix: drop pending webview view revival on cancellation (fixes #320623) - #327583

Open
vs-code-engineering[bot] wants to merge 2 commits into
mainfrom
fix/webviewview-awaiting-revival-320623-4db1cdc0fbe13d3b
Open

fix: drop pending webview view revival on cancellation (fixes #320623)#327583
vs-code-engineering[bot] wants to merge 2 commits into
mainfrom
fix/webviewview-awaiting-revival-320623-4db1cdc0fbe13d3b

Conversation

@vs-code-engineering

@vs-code-engineering vs-code-engineering Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

WebviewViewService.resolve() throws Error: View already awaiting revival when a webview view pane calls resolve() for a view type that already has a pending _awaitingRevival entry and no resolver has registered yet. This happens because a pending revival entry is never removed when the pane that requested it is torn down or re-activated — the second resolve() for the same view type then hits the guard at webviewViewService.ts:131 and throws. With 1,848 affected users on 1.123.0 across all platforms, this is a benign-but-noisy lifecycle race: the cancellation token that resolve() receives was previously ignored entirely.

The fix makes the pending-revival entry honor cancellation (producer/service side) and makes the pane actually cancel its token when the webview is torn down.

Fixes #320623
Recommended reviewer: @mjbvz

Culprit Commit

Not a single-commit regression. git blame shows the _awaitingRevival guard (webviewViewService.ts:130-131) dates to 61f799f53bc1 (Matt Bierner, 2020-08-20) and the promiseWithResolvers pending entry to ab5dd705b046 (2024-01-30). The bucket is flagged as a stable anomaly first appearing in 1.123.0 (6a44c352) with a possible prior occurrence (#227624, closed as not_planned), and the commit range 8761a556...6a44c352 could not be narrowed (not reproduced in insider builds). This is best classified as a pre-existing latent race that surfaced at stable scale rather than a specific culprit commit; the cancellation parameter has been unused since the pending-revival path was introduced.

Code Flow

sequenceDiagram
    participant Pane as WebviewViewPane.activate
    participant Svc as WebviewViewService
    Pane->>Svc: resolve(id, webviewView, token) [no resolver yet]
    Svc->>Svc: _awaitingRevival.set(id, {webview, resolve})
    Note over Pane: pane hidden / disposed / re-activated
    Pane-->>Pane: _webviewDisposables.clear() (token NOT cancelled before fix)
    Pane->>Svc: resolve(id, webviewView2, token2) [still no resolver]
    Svc->>Svc: _awaitingRevival.has(id) === true
    Svc-->>Pane: throw Error('View already awaiting revival')
Loading

Affected Files

  • src/vs/workbench/contrib/webviewView/browser/webviewViewService.tsresolve() now registers a cancellation.onCancellationRequested handler that removes the pending _awaitingRevival entry (guarded by identity check on resolve) and settles the promise.
  • src/vs/workbench/contrib/webviewView/browser/webviewViewPane.tsactivate() now cancels the CancellationTokenSource when _webviewDisposables is cleared (dispose or re-activation), so the service is notified to drop the pending entry.

Repro Steps

  1. Open a view contributed by an extension as a webview view before the extension's resolver has registered (slow activation), so a _awaitingRevival entry is created.
  2. Hide/collapse the view (or trigger re-activation) so the pane tears down _webviewDisposables while the resolver still hasn't registered.
  3. Re-show the view: activate() calls resolve() again for the same view id → View already awaiting revival is thrown. Frequent at stable scale during startup/extension-activation windows.

How the Fix Works

Chosen approach — fix at the producer of the stale state, not the crash site (the guard at webviewViewService.ts:131 is a symptom):

  • webviewViewService.ts: the _awaitingRevival entry is the state that goes stale when its requesting pane disappears. The service now subscribes to the caller's cancellation token and, on cancellation, deletes the pending entry (only if it is still the same entry — identity check on resolve) and settles its promise. This makes the previously-unused cancellation parameter meaningful and ensures a later resolve() for the same view type takes the fresh path instead of throwing.
  • webviewViewPane.ts: the pane owns the CancellationTokenSource in _webviewDisposables. CancellationTokenSource.dispose() does not cancel, so a toDisposable(() => source.cancel()) is added to the same store; when the store is cleared (on pane dispose or re-activation via the dispose callback / teardown), the token is cancelled and the service drops the pending entry. This closes the lifecycle gap rather than tolerating the inconsistent state.

No try/catch was added and the throw path is preserved — it simply becomes unreachable in the torn-down-pane scenario because the pending entry is cleaned up at its producer.

Alternatives considered

  • Swallowing the throw at resolve() with a try/catch or replacing throw with an early return: hides the lifecycle bug instead of fixing the producer, and would leave the stale _awaitingRevival entry in place, breaking future genuine revivals.
  • Deleting the pending entry unconditionally inside register(): does not address teardown that happens before any resolver registers, which is the actual failing sequence.

Recommended Owner

@mjbvz — original author and long-time owner of the webview view service and pane (webviewViewService.ts, webviewViewPane.ts).

Generated by errors-fix · opus48 · 252.4 AIC · ⌖ 11.6 AIC · ⊞ 18.1K ·


errors-fix-driver — cycle 1

Trigger: cron_review_comments · Head: bdaa13976ca524313af9618d9610d9af69082543 (bdaa1397)

Item Action
Review on webviewViewService.ts:145 (in scope, leak) Fixed in bdaa1397 — dispose cancellation subscription on settle (replied + resolved)
Review on webviewViewService.ts:145 (in scope, duplicate leak) Fixed in bdaa1397 (replied + resolved)
Review on webviewViewService.ts:139 (in scope, comment wording) Reworded in bdaa1397 (replied + resolved)
Review on webviewViewPane.ts:205 (subjective, LIFO order) Replied explaining tradeoff + resolved (no code change — no ordering dependency)

Push: yes — bdaa1397 · Copilot rerequested: ok

Ready gate: CI in progress on the new head (re-triggered by this push) and Copilot has not re-reviewed the new SHA yet → not marking ready this cycle

Generated by errors-fix-driver · 1.1K AIC · ⌖ 34.7 AIC · ⊞ 72.7K ·

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 27, 2026 06:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot could not run the full agentic suite for this review because it was automatically requested on a bot-authored pull request. Request a review from Copilot under Reviewers to retry with the full agentic suite. Improved support for bot-authored pull requests is coming soon.

This PR improves cancellation handling for webview view revival so pending “awaiting revival” state is cleaned up when a view is torn down, preventing stale entries and related errors.

Changes:

  • Remove pending _awaitingRevival entries when the caller’s cancellation token is triggered.
  • Trigger cancellation for in-flight revival when the webview is torn down.

Reviewed changes

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

File Description
src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts Cleans up _awaitingRevival on cancellation to prevent stale map entries and “already awaiting revival” errors.
src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts Cancels the revival token source during webview teardown to drive cleanup in the service.

Comment on lines +140 to +145
cancellation.onCancellationRequested(() => {
if (this._awaitingRevival.get(viewType)?.resolve === resolve) {
this._awaitingRevival.delete(viewType);
resolve();
}
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bdaa1397: the onCancellationRequested subscription is now captured and disposed on both settle paths — in the cancellation handler itself, and in register() when a resolver revives the pending entry — so the listener no longer retains the closures or accumulates on the token.

Generated by errors-fix-driver · 1.1K AIC · ⌖ 34.7 AIC · ⊞ 72.7K


// Cancel in-flight revival when the webview is torn down (dispose or re-activation)
// so the webview view service drops any pending `_awaitingRevival` entry for this view.
this._webviewDisposables.add(toDisposable(() => source.cancel()));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving the cancel hook in _webviewDisposables intentionally. Correctness here does not depend on disposal order: the only requirement is that source.cancel() runs at some point during teardown so the service drops the pending _awaitingRevival entry. The cancel hook has no ordering dependency on the other webview disposables (drag listeners, state listener, etc.), so LIFO vs. FIFO makes no observable difference — the token fires and the service cleans up regardless. Keeping it co-located in the same store also ensures it can never be leaked if teardown paths change. If a future change adds a disposable that must observe the not-yet-cancelled token, we can revisit, but that dependency does not exist today.

Generated by errors-fix-driver · 1.1K AIC · ⌖ 34.7 AIC · ⊞ 72.7K

@vs-code-engineering
vs-code-engineering Bot requested a review from Copilot July 27, 2026 06:10
@vs-code-engineering
vs-code-engineering Bot marked this pull request as ready for review July 27, 2026 06:10
@vs-code-engineering
vs-code-engineering Bot enabled auto-merge (squash) July 27, 2026 06:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

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

Comment on lines +140 to +145
cancellation.onCancellationRequested(() => {
if (this._awaitingRevival.get(viewType)?.resolve === resolve) {
this._awaitingRevival.delete(viewType);
resolve();
}
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bdaa1397: the subscription returned by cancellation.onCancellationRequested(...) is now stored alongside the _awaitingRevival entry and disposed whenever the entry settles (either cancellation here or revival in register()), avoiding the retained-closure / listener-accumulation concern.

Generated by errors-fix-driver · 1.1K AIC · ⌖ 34.7 AIC · ⊞ 72.7K

Comment on lines +137 to +139
// If the caller is cancelled (e.g. the view pane is torn down or re-activated)
// before a resolver registers, drop the pending entry so a later resolve for the
// same view type does not throw "View already awaiting revival".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded in bdaa1397: the comment now says the cleanup runs "at any point before the pending entry is revived by a resolver" rather than implying it only applies before a resolver registers.

Generated by errors-fix-driver · 1.1K AIC · ⌖ 34.7 AIC · ⊞ 72.7K

…ubscription on settle

Dispose the onCancellationRequested subscription on both settle paths
(cancellation and revival) so it does not retain closures or accumulate
listeners. Also clarify the comment to reflect that cleanup runs any time
before the pending entry is revived, not only before a resolver registers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@vs-code-engineering

Copy link
Copy Markdown
Contributor Author

Commit pushed: 75144e8

Generated by errors-fix-driver · 1.1K AIC · ⌖ 34.7 AIC · ⊞ 72.7K

@vs-code-engineering
vs-code-engineering Bot requested a review from Copilot July 27, 2026 06:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment on lines +143 to +149
const subscription = cancellation.onCancellationRequested(() => {
if (this._awaitingRevival.get(viewType)?.resolve === resolve) {
this._awaitingRevival.delete(viewType);
subscription.dispose();
resolve();
}
});
Comment on lines 116 to 120
resolver.resolve(pending.webview, CancellationToken.None).then(() => {
this._awaitingRevival.delete(viewType);
pending.subscription.dispose();
pending.resolve();
});
Comment on lines +203 to +205
// Cancel in-flight revival when the webview is torn down (dispose or re-activation)
// so the webview view service drops any pending `_awaitingRevival` entry for this view.
this._webviewDisposables.add(toDisposable(() => source.cancel()));
@alexr00
alexr00 disabled auto-merge July 27, 2026 08:53
@alexr00 alexr00 closed this Jul 27, 2026
@bryanchen-d bryanchen-d reopened this Jul 27, 2026
@bryanchen-d

Copy link
Copy Markdown
Contributor

assigned to @benibenj as this is to do with feature area: Workbench → Views.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Error] unhandlederror-View already awaiting revival

7 participants