Skip to content

fix: Race condition between HTML parser disposal and concurrent parsing - #1909

Merged
linkdotnet merged 1 commit into
mainfrom
fix/#1892
Sep 4, 2026
Merged

fix: Race condition between HTML parser disposal and concurrent parsing#1909
linkdotnet merged 1 commit into
mainfrom
fix/#1892

Conversation

@linkdotnet

Copy link
Copy Markdown
Collaborator

Closes #1892

Copilot AI lite review requested due to automatic review settings September 4, 2026 10:57

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.

🟡 Changes recommended

The new test and changelog currently claim “waiting” semantics that are not actually asserted/implemented, so they should be corrected for accuracy (and ideally strengthened with a deterministic regression test for the race).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR addresses an intermittent teardown race where BunitHtmlParser.Dispose() could enumerate its internal document list while another thread was still parsing and adding documents, causing InvalidOperationException: Collection was modified.

Changes:

  • Serialize BunitHtmlParser.Parse() and Dispose() via a shared lock to prevent concurrent mutation/enumeration of tracked documents.
  • Add a new test intended to cover disposal during an active markup assertion.
  • Document the fix in the changelog.
File summaries
File Description
src/bunit/Rendering/BunitHtmlParser.cs Adds a lock to serialize parsing and disposal to prevent concurrent access to the internal documents list.
tests/bunit.tests/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.cs Adds a regression-style test around disposal while markup assertions run on the renderer dispatcher.
CHANGELOG.md Adds an Unreleased “Fixed” entry describing the change.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 69 to +82
public INodeList Parse([StringSyntax("Html")] string markup)
{
ArgumentNullException.ThrowIfNull(markup);

var document = GetNewDocumentAsync().GetAwaiter().GetResult();
lock (parserLock)
{
var document = GetNewDocumentAsync().GetAwaiter().GetResult();

var (ctx, matchedElement) = GetParseContext(markup, document);
var (ctx, matchedElement) = GetParseContext(markup, document);

return ctx is null && matchedElement is not null
? ParseSpecial(markup, matchedElement)
: htmlParser.ParseFragment(markup, ctx!);
return ctx is null && matchedElement is not null
? ParseSpecial(markup, matchedElement)
: htmlParser.ParseFragment(markup, ctx!);
}
Comment thread CHANGELOG.md Outdated

### Fixed

- Synchronous context disposal now waits for queued renderer work, and HTML parsing is serialized with parser disposal, preventing teardown races. Reported by [@thimobuchheister](https://github.com/thimobuchheister) in #1892. Fixed by [@linkdotnet](https://github.com/linkdotnet).
Comment on lines +243 to +244
[Fact(DisplayName = "Disposing a context waits for an active markup assertion on the renderer dispatcher")]
public async Task Context_disposal_waits_for_active_markup_assertion()
private readonly IBrowsingContext context;
private readonly HtmlParser htmlParser;
private readonly List<IDocument> documents = new();
private readonly object parserLock = new();

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.

Can we use the Lock type instead of object?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We have to #if NET9_0_OR_GREATER - we never use System.Lock until now but that can be done in bunit v3

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.

right, over and out :D

@linkdotnet

Copy link
Copy Markdown
Collaborator Author

This closes / superseeds #1893
#1893 anyway does not pass out test suite

@linkdotnet

Copy link
Copy Markdown
Collaborator Author

I would merge if there are no reasons against it

@linkdotnet
linkdotnet merged commit 69f4038 into main Sep 4, 2026
10 checks passed
@linkdotnet
linkdotnet deleted the fix/#1892 branch September 4, 2026 14:29
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.

BunitHtmlParser.Dispose() races Parse() on another thread: "Collection was modified" during test teardown

3 participants