From e9346869a281e8539fc7372fe158b79387bd5c60 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 28 Oct 2025 16:16:37 +1100 Subject: [PATCH 1/2] chore: fix TestHttpServer test flake --- Tests.Vpn.Service/TestHttpServer.cs | 50 ++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/Tests.Vpn.Service/TestHttpServer.cs b/Tests.Vpn.Service/TestHttpServer.cs index d33697f..a828616 100644 --- a/Tests.Vpn.Service/TestHttpServer.cs +++ b/Tests.Vpn.Service/TestHttpServer.cs @@ -62,16 +62,7 @@ public TestHttpServer(Func handler) _listenerThread = new Thread(() => { - while (!_cts.Token.IsCancellationRequested) - try - { - var context = _listener.GetContext(); - Task.Run(() => HandleRequest(context)); - } - catch (HttpListenerException) when (_cts.Token.IsCancellationRequested) - { - break; - } + RequestLoop().GetAwaiter().GetResult(); }); _listenerThread.Start(); @@ -81,10 +72,47 @@ public void Dispose() { _cts.Cancel(); _listener.Stop(); - _listenerThread.Join(); + try + { + _listenerThread.Join(); + } + catch (ThreadStateException) + { + // Ignore if the listener thread is already dead + } + catch (ThreadInterruptedException) + { + // Ignore interrupted listener thread, it's now closed anyway + } GC.SuppressFinalize(this); } + private async Task RequestLoop() + { + while (!_cts.Token.IsCancellationRequested) + try + { + var contextTask = _listener.GetContextAsync(); + // Wait with a cancellation token. + await contextTask.WaitAsync(_cts.Token); + // Get the context or throw if there was an error. + var context = await contextTask; + // Run the handler in the background. + _ = Task.Run(() => HandleRequest(context)); + } + catch (HttpListenerException) when (_cts.Token.IsCancellationRequested) + { + // Ignore, we expect the listener to throw an exception when + // it's stopped + break; + } + catch (TaskCanceledException) + { + // Ignore, the CTS was cancelled + break; + } + } + private async Task HandleRequest(HttpListenerContext context) { try From d9d043114008057efa8106a6cef5440da7d16734 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 28 Oct 2025 17:07:34 +1100 Subject: [PATCH 2/2] avoid wrapper thread --- Tests.Vpn.Service/TestHttpServer.cs | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/Tests.Vpn.Service/TestHttpServer.cs b/Tests.Vpn.Service/TestHttpServer.cs index a828616..0d7c9e4 100644 --- a/Tests.Vpn.Service/TestHttpServer.cs +++ b/Tests.Vpn.Service/TestHttpServer.cs @@ -13,7 +13,7 @@ public class TestHttpServer : IDisposable private readonly CancellationTokenSource _cts = new(); private readonly Func _handler; private readonly HttpListener _listener; - private readonly Thread _listenerThread; + private readonly Task _listenerTask; public string BaseUrl { get; private set; } @@ -60,12 +60,7 @@ public TestHttpServer(Func handler) throw new InvalidOperationException("Could not find a free port to listen on"); BaseUrl = $"http://localhost:{port}"; - _listenerThread = new Thread(() => - { - RequestLoop().GetAwaiter().GetResult(); - }); - - _listenerThread.Start(); + _listenerTask = RequestLoop(); } public void Dispose() @@ -74,15 +69,11 @@ public void Dispose() _listener.Stop(); try { - _listenerThread.Join(); - } - catch (ThreadStateException) - { - // Ignore if the listener thread is already dead + _listenerTask.GetAwaiter().GetResult(); } - catch (ThreadInterruptedException) + catch (TaskCanceledException) { - // Ignore interrupted listener thread, it's now closed anyway + // Ignore } GC.SuppressFinalize(this); } @@ -106,11 +97,6 @@ private async Task RequestLoop() // it's stopped break; } - catch (TaskCanceledException) - { - // Ignore, the CTS was cancelled - break; - } } private async Task HandleRequest(HttpListenerContext context)