Skip to content

fix(frame): subscribe before checking state in navigation waits - #3384

Closed
Yury Semikhatsky (yury-s) wants to merge 2 commits into
mainfrom
fix-3364
Closed

Yury Semikhatsky (yury-s) wants to merge 2 commits into
mainfrom
fix-3364

Conversation

@yury-s

@yury-s Yury Semikhatsky (yury-s) commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Frame events are raised on the transport thread, while WaitForURLAsync, WaitForLoadStateAsync and WaitForNavigationAsync checked the recorded state first and subscribed afterwards. A navigated or loadstate event delivered in between was never seen again, and the wait ran to its timeout. The JS client does the same check-then-subscribe, but there nothing can be dispatched between the two.
  • Subscribe before checking in those three places, keeping the methods' structure otherwise; WaitForURLAsync re-checks the URL inside the navigation wait once the subscription exists.
  • The regression test is the issue's scenario (page A navigates to B a few ms after load, WaitForURLAsync("**/b.html") right after GotoAsync); it fails on main in a fresh test host, where the check-to-subscribe window is widest.

Fixes: #3364

Comment thread src/Playwright/Core/Frame.cs Outdated
// Subscribe before checking: load states are recorded and the event is raised on the
// transport thread, and an event raised between the check and the subscription would
// never be seen again (https://github.com/microsoft/playwright-dotnet/issues/3364).
var (loadStateTask, unsubscribe) = waiter.GetWaitForEventTask<WaitUntilState>(this, "LoadState", s =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is different from what we do in JS

Frame events are raised on the transport thread, but WaitForURLAsync,
WaitForLoadStateAsync and WaitForNavigationAsync checked the recorded
state first and subscribed afterwards. A navigation or load state
delivered in between was never seen again and the wait ran to its
timeout. Subscribe first in all three places, guard the load-state list
that both threads touch, and wait for load states by WaitUntilState so
that Commit is honored when the URL already matches.

Fixes: #3364
Review follow-up: no lock around _loadStates, the three wait methods keep
their original structure, and the only deviation from the JS client is
subscribing before the state check, with a comment at each spot.
WaitForURLAsync re-checks the URL inside the navigation wait once the
subscription exists instead of restructuring the method.
{
waiter = SetupNavigationWaiter("frame.WaitForLoadStateAsync", options?.Timeout);

// Unlike the JS client, load states are updated on the transport thread, so subscribe

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can't afford this - everything else will fall apart. We should make sure events come back as a flow serialized with commands on the api thread. Otherwise all of our assumptions are invalid.

@dgozman

Copy link
Copy Markdown
Collaborator

Closing in favor of #3453.

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.

[Bug]: WaitForURLAsync can lose the event it is waiting for — its check-then-subscribe races with the thread that delivers events

3 participants