Skip to content

fix: prevent concurrent HttpContent matching from corrupting the stream position - #875

Merged
vbreuss merged 2 commits into
mainfrom
topic/fix-concurrent-http-content-matching
Oct 1, 2026
Merged

vbreuss merged 2 commits into
mainfrom
topic/fix-concurrent-http-content-matching

Conversation

@vbreuss

@vbreuss vbreuss commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

HttpContent caches the stream returned by ReadAsStream, so the string and binary content matchers share it when a setup and a verification match the same request concurrently (e.g. a polling verification with Within while the request is still being matched against the setups). Interleaved save/restore of the stream position could leave it at the end, after which every match read an empty body and the verification never succeeded. The read and position reset are now done under a lock on the HttpContent.

…am position

HttpContent caches the stream returned by ReadAsStream, so the string and binary content matchers share it when a setup and a verification match the same request concurrently (e.g. a polling verification with Within while the request is still being matched against the setups). Interleaved save/restore of the stream position could leave it at the end, after which every match read an empty body and the verification never succeeded. The read and position reset are now done under a lock on the stream.
@vbreuss vbreuss self-assigned this Oct 1, 2026
Copilot AI balanced review requested due to automatic review settings October 1, 2026 11:18
@vbreuss vbreuss added the bug Something isn't working label Oct 1, 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.

Copilot review overview

🟢 Approval recommended

The locking directly addresses the stream-position race and is covered by focused regression tests.

Review effort: Balanced
Findings: None

What changed in this PR

Prevents concurrent HTTP body matchers from corrupting a shared cached stream position.

Changes:

  • Synchronizes string and binary stream reads and position restoration.
  • Adds concurrent verification regression tests for both matcher types.
File Description
Source/​Mockolate/​Web/​ItExtensions.HttpContent.cs Locks shared content streams during matching.
Tests/​Mockolate.Tests/​Web/​ItExtensionsTests.IsHttpContentTests.WithStringTests.cs Tests concurrent string matching.
Tests/​Mockolate.Tests/​Web/​ItExtensionsTests.IsHttpContentTests.WithBytesTests.cs Tests concurrent binary matching.

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

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Test Results

    24 files  ± 0      24 suites  ±0   12m 3s ⏱️ +20s
 4 524 tests + 2   4 522 ✅ + 2  2 💤 ±0  0 ❌ ±0 
28 649 runs  +14  28 645 ✅ +14  4 💤 ±0  0 ❌ ±0 

Results for commit 1df5163. ± Comparison against base commit 809c0a3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.5 LTS (Noble Numbat)
AMD EPYC 7763 2.83GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.401
[Host] : .NET 10.0.12 (10.0.12, 10.0.1226.42308), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Method N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 362.2 ns 7.25 ns 6.78 ns 1.03 1.93 KB 1.00
Mockolate 1 351.3 ns 11.96 ns 11.19 ns 1.00 1.93 KB 1.00
Imposter 1 593.6 ns 25.15 ns 23.52 ns 1.69 4.04 KB 2.09
TUnitMocks 1 496.1 ns 11.27 ns 10.55 ns 1.41 2.02 KB 1.04
Moq 1 188,479.1 ns 678.17 ns 601.18 ns 536.98 14.58 KB 7.55
NSubstitute 1 6,261.0 ns 43.75 ns 38.78 ns 17.84 9.12 KB 4.72
FakeItEasy 1 6,151.4 ns 84.28 ns 78.84 ns 17.53 8.05 KB 4.17
baseline* 10 670.3 ns 8.07 ns 7.15 ns 0.95 2.14 KB 1.00
Mockolate 10 702.9 ns 8.72 ns 7.73 ns 1.00 2.14 KB 1.00
Imposter 10 1,233.8 ns 14.02 ns 12.43 ns 1.76 5.52 KB 2.58
TUnitMocks 10 1,410.9 ns 9.70 ns 8.60 ns 2.01 3.73 KB 1.74
Moq 10 193,348.6 ns 1,107.13 ns 981.44 ns 275.12 18.48 KB 8.63
NSubstitute 10 9,196.1 ns 74.86 ns 70.02 ns 13.09 12.07 KB 5.64
FakeItEasy 10 9,510.2 ns 93.99 ns 87.91 ns 13.53 15.42 KB 7.20
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.5 LTS (Noble Numbat)
AMD EPYC 9V74 3.69GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.401
[Host] : .NET 10.0.12 (10.0.12, 10.0.1226.42308), X64 RyuJIT x86-64-v4

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Event Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 352.0 ns 7.08 ns 6.63 ns 1.49 1.7 KB 1.00
Mockolate 235.6 ns 4.93 ns 4.62 ns 1.00 1.7 KB 1.00
Imposter 1,106.9 ns 28.68 ns 26.83 ns 4.70 8.8 KB 5.17
TUnitMocks 142.8 ns 2.34 ns 2.08 ns 0.61 1.34 KB 0.79
Moq 11,247.4 ns 75.58 ns 70.70 ns 47.75 12.51 KB 7.34
NSubstitute 4,431.2 ns 68.29 ns 63.88 ns 18.81 9.05 KB 5.31
FakeItEasy 183,948.7 ns 674.84 ns 598.23 ns 780.97 15.26 KB 8.96
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.5 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.401
[Host] : .NET 10.0.12 (10.0.12, 10.0.1226.42308), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Callback Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 248.6 ns 1.94 ns 1.62 ns 0.71 1.57 KB 1.00
Mockolate 347.7 ns 11.84 ns 11.08 ns 1.00 1.57 KB 1.00
Imposter 460.8 ns 7.60 ns 7.11 ns 1.33 2.38 KB 1.52
TUnitMocks 525.6 ns 24.01 ns 22.46 ns 1.51 1.99 KB 1.27
Moq 100,279.0 ns 692.95 ns 614.28 ns 288.67 8.88 KB 5.66
NSubstitute 4,869.4 ns 73.86 ns 69.09 ns 14.02 7.71 KB 4.91
FakeItEasy 5,157.6 ns 29.66 ns 26.29 ns 14.85 6.81 KB 4.33
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.5 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.401
[Host] : .NET 10.0.12 (10.0.12, 10.0.1226.42308), X64 RyuJIT x86-64-v4

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Indexer N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 979.2 ns 29.07 ns 27.20 ns 1.20 3.77 KB 1.00
Mockolate 1 812.9 ns 40.63 ns 38.00 ns 1.00 3.77 KB 1.00
Imposter 1 718.6 ns 17.38 ns 16.26 ns 0.89 5.16 KB 1.37
Moq 1 129,498.3 ns 1,376.78 ns 1,220.48 ns 159.62 20.36 KB 5.41
NSubstitute 1 7,592.4 ns 132.89 ns 124.31 ns 9.36 12.84 KB 3.41
FakeItEasy 1 8,111.7 ns 101.62 ns 90.09 ns 10.00 13.63 KB 3.62
baseline* 10 2,548.5 ns 36.15 ns 33.81 ns 1.21 4.82 KB 1.00
Mockolate 10 2,103.7 ns 32.64 ns 30.53 ns 1.00 4.82 KB 1.00
Imposter 10 2,002.7 ns 20.23 ns 17.93 ns 0.95 7.97 KB 1.65
Moq 10 138,095.5 ns 1,109.89 ns 926.80 ns 65.66 28.76 KB 5.97
NSubstitute 10 17,997.6 ns 330.49 ns 309.14 ns 8.56 25.63 KB 5.32
FakeItEasy 10 16,720.4 ns 116.75 ns 109.21 ns 7.95 32.97 KB 6.84
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.5 LTS (Noble Numbat)
INTEL XEON PLATINUM 8573C 2.30GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.401
[Host] : .NET 10.0.12 (10.0.12, 10.0.1226.42308), X64 RyuJIT x86-64-v4

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

CreateMock Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 18.41 ns 0.803 ns 0.671 ns 1.30 160 B 1.00
Mockolate 14.16 ns 0.042 ns 0.040 ns 1.00 160 B 1.00
Imposter 220.78 ns 1.007 ns 0.892 ns 15.59 2248 B 14.05
TUnitMocks 30.42 ns 0.063 ns 0.055 ns 2.15 200 B 1.25
Moq 920.95 ns 1.621 ns 1.516 ns 65.02 2096 B 13.10
NSubstitute 1,457.41 ns 4.778 ns 4.236 ns 102.89 5048 B 31.55
FakeItEasy 1,064.69 ns 3.053 ns 2.856 ns 75.17 2759 B 17.24
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.5 LTS (Noble Numbat)
AMD EPYC 9V74 3.64GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.401
[Host] : .NET 10.0.12 (10.0.12, 10.0.1226.42308), X64 RyuJIT x86-64-v4

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Property N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 292.1 ns 1.52 ns 1.42 ns 0.71 2.41 KB 1.00
Mockolate 1 410.4 ns 10.84 ns 10.14 ns 1.00 2.41 KB 1.00
Imposter 1 365.5 ns 7.61 ns 7.12 ns 0.89 3.13 KB 1.29
TUnitMocks 1 375.0 ns 9.54 ns 8.93 ns 0.91 1.64 KB 0.68
Moq 1 8,154.3 ns 111.59 ns 104.38 ns 19.88 10.39 KB 4.30
NSubstitute 1 5,504.2 ns 125.63 ns 117.52 ns 13.42 11.45 KB 4.74
FakeItEasy 1 5,836.8 ns 156.33 ns 146.23 ns 14.23 11.24 KB 4.66
baseline* 10 694.8 ns 1.94 ns 1.82 ns 0.83 2.91 KB 1.00
Mockolate 10 832.9 ns 13.30 ns 11.79 ns 1.00 2.91 KB 1.00
Imposter 10 861.6 ns 12.19 ns 11.40 ns 1.03 4.67 KB 1.61
TUnitMocks 10 1,357.7 ns 26.46 ns 24.75 ns 1.63 3.94 KB 1.35
Moq 10 13,394.1 ns 171.69 ns 152.20 ns 16.08 18.28 KB 6.29
NSubstitute 10 12,463.1 ns 230.94 ns 216.03 ns 14.97 21.08 KB 7.25
FakeItEasy 10 13,498.1 ns 303.06 ns 268.65 ns 16.21 30.81 KB 10.60

baseline* rows show the corresponding Mockolate benchmark from the most recent successful main branch build with results, for regression comparison.

Stream derives from MarshalByRefObject, which SonarCloud flags as an unsafe lock target. The read stream is cached per HttpContent, so locking on the content instance protects the same shared stream.
Copilot AI balanced review requested due to automatic review settings October 1, 2026 12:30

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.

Copilot review overview

🟡 Changes recommended

The locks target HttpContent rather than the shared stream, leaving concurrent access through separate wrappers unsafe.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)

Comment thread Source/Mockolate/Web/ItExtensions.HttpContent.cs
@sonarqubecloud

sonarqubecloud Bot commented Oct 1, 2026

Copy link
Copy Markdown

@vbreuss
vbreuss merged commit 41c85a6 into main Oct 1, 2026
19 checks passed
@vbreuss
vbreuss deleted the topic/fix-concurrent-http-content-matching branch October 1, 2026 12:52
github-actions Bot added a commit that referenced this pull request Oct 1, 2026
…ching from corrupting the stream position (#875) by Valentin Breuß
github-actions Bot added a commit that referenced this pull request Oct 1, 2026
…ching from corrupting the stream position (#875) by Valentin Breuß
@github-actions github-actions Bot added the state: released The issue is released label Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

This is addressed in release v3.5.2.

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

Labels

bug Something isn't working state: released The issue is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants